Commit 3354044e authored by anton.shloma's avatar anton.shloma

Devel, config split, config ignore and small fix for install

parent f54e75f5
...@@ -34,7 +34,10 @@ ...@@ -34,7 +34,10 @@
"drupal/slick": "^1.1", "drupal/slick": "^1.1",
"drupal/slick_entityreference": "^1.1", "drupal/slick_entityreference": "^1.1",
"drupal/video_embed_field": "^2.0", "drupal/video_embed_field": "^2.0",
"drush/drush": "^9.5" "drush/drush": "^9.5",
"drupal/devel": "^1.2",
"drupal/config_split": "^1.4",
"drupal/config_ignore": "^2.1"
}, },
"replace": { "replace": {
"drupal/core": "^8.6" "drupal/core": "^8.6"
......
This diff is collapsed.
language: php
php:
- 5.6
- 7.0
env:
global:
- PATH=$PATH:/home/travis/.composer/vendor/bin
install:
- composer self-update
before_script:
# Set sendmail so drush doesn't throw an error during site install.
- echo "sendmail_path=`which true`" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'`
# Create database.
- mysql -e 'create database drupal'
# Install Drupal 8 target site.
- cd tests
- mkdir -p themes modules profiles
- composer install --prefer-dist
- ./vendor/bin/drush si standard -y --db-url=mysql://travis:@127.0.0.1/drupal
# Test latest commit on current branch.
- git clone --branch=$TRAVIS_BRANCH https://github.com/$TRAVIS_REPO_SLUG.git modules/config_filter
- ./vendor/bin/drush en config_filter -y
# Run Drush web server.
- ./vendor/bin/drush --debug runserver :8888 > ~/debug.txt 2>&1 &
- sleep 4s
- chmod -R ug+w sites
script:
- cd $TRAVIS_BUILD_DIR
- cd tests
- ./vendor/bin/phpcs --config-set installed_paths ../../drupal/coder/coder_sniffer
- ./vendor/bin/phpcs --config-set show_progress 1
- ./vendor/bin/phpcs --warning-severity=0 --standard=Drupal,DrupalPractice ../src
- ./vendor/bin/phpunit
notifications:
email: false
This diff is collapsed.
# Config Filter
[![Build Status](https://travis-ci.org/nuvoleweb/config_filter.svg?branch=8.x-1.x)](https://travis-ci.org/nuvoleweb/config_filter)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nuvoleweb/config_filter/badges/quality-score.png?b=8.x-1.x)](https://scrutinizer-ci.com/g/nuvoleweb/config_filter/?branch=8.x-1.x)
## Introduction
Modules such as Config Split want to modify the configuration when it is
synchronized between the database and the exported yaml files.
This module provides the API to do so but does not influence a sites operation.
## How it works
Configuration Filter swaps the config.storage.sync service from Drupal 8 core.
The new service wraps the file storage and applies filters to it.
This allows other modules to change the configuration as it gets imported or
exported both in the Drupal UI and with drush.
## What is a ConfigFilter
A ConfigFilter is a plugin. This module provides the plugin definition, the
plugin manager and the storage factory.
A ConfigFilter can have the following annotation:
```php
/**
* @ConfigFilter(
* id = "may_plugin_id",
* label = @Translation("An example configuration filter"),
* weight = 0,
* status = TRUE,
* storages = {"config.storage.sync"},
* )
*/
```
See `\Drupal\config_filter\Annotation\ConfigFilter`.
The weight allows the filters to be sorted. The status allows the filter to be
active or inactive, the `ConfigFilterManagerInterface::getFiltersForStorages`
will only take active filters into consideration. The weight, status and
storages are optional and the above values are the default.
## Alternative Config Filter Managers
Plugins are only available from enabled modules. If you want to provide a
config filter from a php library, all you have to do is implement the
`\Drupal\config_filter\ConfigFilterManagerInterface` and add it to the
service container with a `config.filter` tag.
Services with higher priority will have their filters added first.
{
"name": "drupal/config_filter",
"type": "drupal-module",
"description": "Config Filter allows other modules to interact with a ConfigStorage through filter plugins.",
"keywords": ["Drupal", "configuration", "configuration management"],
"authors": [
{
"name": "Fabian Bircher",
"email": "opensource@fabianbircher.com",
"homepage": "https://www.drupal.org/u/bircher",
"role": "Maintainer"
},
{
"name": "Nuvole Web",
"email": "info@nuvole.org",
"homepage": "http://nuvole.org",
"role": "Maintainer"
}
],
"homepage": "https://www.drupal.org/project/config_filter",
"support": {
"issues": "https://www.drupal.org/project/issues/config_filter",
"irc": "irc://irc.freenode.org/drupal-contribute",
"source": "http://cgit.drupalcode.org/config_filter"
},
"license": "GPL-2.0+",
"require": {},
"suggest": {
"drupal/config_split": "Split site configuration for different environments."
}
}
name: Config Filter
type: module
description: Config Filter allows other modules to interact with a ConfigStorage through filter plugins.
# core: 8.x
package: Config
# Information added by Drupal.org packaging script on 2018-11-14
version: '8.x-1.4'
core: '8.x'
project: 'config_filter'
datestamp: 1542184984
services:
plugin.manager.config_filter:
class: Drupal\config_filter\Plugin\ConfigFilterPluginManager
parent: default_plugin_manager
tags:
- { name: config.filter }
config_filter.storage.sync:
class: Drupal\config_filter\Config\FilteredStorage
factory: config_filter.storage_factory:getSync
decorates: config.storage.staging
public: false
config_filter.storage_factory:
class: Drupal\config_filter\ConfigFilterStorageFactory
arguments: ['@config_filter.storage.sync.inner']
tags:
- { name: service_collector, tag: 'config.filter', call: addConfigFilterManager }
<?php
namespace Drupal\config_filter\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Config filter plugin item annotation object.
*
* @see \Drupal\config_filter\Plugin\ConfigFilterPluginManager
* @see plugin_api
*
* @Annotation
*/
class ConfigFilter extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The plugin weight.
*
* The higher the weight the later in the filter order the plugin will be.
*
* @var int
*/
public $weight = 0;
/**
* The status of the plugin.
*
* This is an easy way to turn off plugins.
*
* @var bool
*/
public $status = TRUE;
/**
* The storages the plugin filters on.
*
* If it is left empty ['config.storage.sync'] will be assumed.
* The only storage which is currently filtered is 'config.storage.sync'.
*
* @var string[]
*/
public $storages = [];
}
<?php
namespace Drupal\config_filter\Config;
use Drupal\config_filter\Exception\InvalidStorageFilterException;
use Drupal\Core\Config\StorageInterface;
/**
* Class FilteredStorage.
*
* This class wraps another storage.
* It filters the arguments before passing them on to the storage for write
* operations and filters the result of read operations before returning them.
*
* @package Drupal\config_filter\Config
*/
class FilteredStorage implements FilteredStorageInterface {
/**
* The storage container that we are wrapping.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The storage filters.
*
* @var \Drupal\config_filter\Config\StorageFilterInterface[]
*/
protected $filters;
/**
* Create a FilteredStorage with some storage and a filter.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The decorated storage.
* @param \Drupal\config_filter\Config\StorageFilterInterface[] $filters
* The filters to apply in the given order.
*/
public function __construct(StorageInterface $storage, array $filters) {
$this->storage = $storage;
$this->filters = $filters;
// Set the storage to all the filters.
foreach ($this->filters as $filter) {
if (!$filter instanceof StorageFilterInterface) {
throw new InvalidStorageFilterException();
}
$filter->setSourceStorage(new ReadOnlyStorage($storage));
$filter->setFilteredStorage($this);
}
}
/**
* {@inheritdoc}
*/
public function exists($name) {
$exists = $this->storage->exists($name);
foreach ($this->filters as $filter) {
$exists = $filter->filterExists($name, $exists);
}
return $exists;
}
/**
* {@inheritdoc}
*/
public function read($name) {
$data = $this->storage->read($name);
foreach ($this->filters as $filter) {
$data = $filter->filterRead($name, $data);
}
return $data;
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
$data = $this->storage->readMultiple($names);
foreach ($this->filters as $filter) {
$data = $filter->filterReadMultiple($names, $data);
}
ksort($data);
return array_filter($data);
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
foreach ($this->filters as $filter) {
if ($data) {
$data = $filter->filterWrite($name, $data);
}
else {
// The filterWrite has an array type hint in the interface.
$data = $filter->filterWrite($name, []);
}
}
if ($data) {
return $this->storage->write($name, $data);
}
// The data has been unset, check if it should be deleted.
if ($this->storage->exists($name)) {
foreach ($this->filters as $filter) {
if ($filter->filterWriteEmptyIsDelete($name)) {
return $this->storage->delete($name);
}
}
}
// The data was not written, but it is not an error.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function delete($name) {
$success = TRUE;
foreach ($this->filters as $filter) {
$success = $filter->filterDelete($name, $success);
}
if ($success) {
$success = $this->storage->delete($name);
}
return $success;
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) {
$success = TRUE;
foreach ($this->filters as $filter) {
$success = $filter->filterRename($name, $new_name, $success);
}
if ($success) {
$success = $this->storage->rename($name, $new_name);
}
return $success;
}
/**
* {@inheritdoc}
*/
public function encode($data) {
return $this->storage->encode($data);
}
/**
* {@inheritdoc}
*/
public function decode($raw) {
return $this->storage->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
$data = $this->storage->listAll($prefix);
foreach ($this->filters as $filter) {
$data = $filter->filterListAll($prefix, $data);
}
sort($data);
return $data;
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
$delete = TRUE;
foreach ($this->filters as $filter) {
$delete = $filter->filterDeleteAll($prefix, $delete);
}
if ($delete) {
return $this->storage->deleteAll($prefix);
}
// The filters returned FALSE for $delete, so we delete the names
// individually and allow filters to prevent deleting the config.
foreach ($this->storage->listAll($prefix) as $name) {
$this->delete($name);
}
// The filters wanted to prevent deleting all and were called to delete the
// individual config name, is this a success? Let us say it is.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
$filters = [];
foreach ($this->filters as $key => $filter) {
$filter = $filter->filterCreateCollection($collection);
if ($filter) {
$filters[$key] = $filter;
}
}
return new static($this->storage->createCollection($collection), $filters);
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
$collections = $this->storage->getAllCollectionNames();
foreach ($this->filters as $filter) {
$collections = $filter->filterGetAllCollectionNames($collections);
}
$collections = array_unique($collections);
sort($collections);
return $collections;
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
$collection = $this->storage->getCollectionName();
foreach ($this->filters as $filter) {
$collection = $filter->filterGetCollectionName($collection);
}
return $collection;
}
}
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Essentially the StorageInterface, but knowing that config_filter is used.
*/
interface FilteredStorageInterface extends StorageInterface {
}
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Class GhostStorage.
*
* A GhostStorage acts like the normal Storage it wraps. All reading operations
* return the values of the decorated storage but write operations are silently
* ignored and the ghost pretends that the operation was successful.
*
* @package Drupal\config_filter\Config
*/
class GhostStorage extends ReadOnlyStorage implements StorageInterface {
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function delete($name) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
return TRUE;
}
}
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
use Drupal\config_filter\Exception\UnsupportedMethod;
/**
* Class ReadOnlyStorage.
*
* This is like any other StorageInterface, except it does not allow writing.
*
* @package Drupal\config_filter\Config
*/
class ReadOnlyStorage implements StorageInterface {
/**
* The config storage that we are decorating.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* Create a ReadOnlyStorage decorating another storage.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The decorated storage.
*/
public function __construct(StorageInterface $storage) {
$this->storage = $storage;
}
/**
* {@inheritdoc}
*/
public function exists($name) {
return $this->storage->exists($name);
}
/**
* {@inheritdoc}
*/
public function read($name) {
return $this->storage->read($name);
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
return $this->storage->readMultiple($names);
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function delete($name) {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function encode($data) {
return $this->storage->encode($data);
}
/**
* {@inheritdoc}
*/
public function decode($raw) {
return $this->storage->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
return $this->storage->listAll($prefix);
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static($this->storage->createCollection($collection));
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return $this->storage->getAllCollectionNames();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->storage->getCollectionName();
}
}
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Interface StorageFilterInterface.
*
* This interface defines a config storage filter as the FilteredStorage expects
* to use when filtering the operations on the storage. The ConfigFilter plugin
* interface extends this interface, together with plugin related interfaces.
*
* A well-behaved filter does not perform any write operation in a read method.
*
* @package Drupal\config_split\Config
*/
interface StorageFilterInterface {
/**
* Sets the source config storage on which the operation is performed.
*
* The storage is given to the filter when the storage wrapper is set up,
* to avoid passing the storage to each of the filters so that they can read
* from it before writing filtered config. The storage is read-only, use the
* decorated storage to allow all filters to work for write operations.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage on which the operation is performed.
*/
public function setSourceStorage(StorageInterface $storage);
/**
* Sets the wrapped config storage which is using the filter.
*
* This storage is available to the filter in order to inspect how the end
* result looks like. This is useful for reading configuration from the
* storage as drupal will. Beware of recursive calls to the filter.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage which has the filters applied.
*/
public function setFilteredStorage(StorageInterface $storage);
/**
* Filters configuration data after it is read from the storage.
*
* @param string $name
* The name of a configuration object to load.
* @param array|bool $data
* The configuration data to filter.
*
* @return array|bool
* The filtered data.
*/
public function filterRead($name, $data);
/**
* Filter configuration data before it is written to the storage.
*
* @param string $name
* The name of a configuration object to save.
* @param array $data
* The configuration data to filter.
*
* @return array|null
* The filtered data.
*/
public function filterWrite($name, array $data);
/**
* Let the filter decide whether not-writing data should mean delete.
*
* Filters can return NULL for `filterWrite($name, $data)` which means to
* not write the data to the source storage, but it can also mean deleting it.
*
* @param string $name
* The name of a configuration object to save.
*
* @return bool|null
* True to delete at the end of a filtered write action.
*/
public function filterWriteEmptyIsDelete($name);
/**
* Filters whether a configuration object exists.
*
* @param string $name
* The name of a configuration object to test.
* @param bool $exists
* The previous result to alter.
*
* @return bool
* TRUE if the configuration object exists, FALSE otherwise.
*/
public function filterExists($name, $exists);
/**
* Deletes a configuration object from the storage.
*
* @param string $name
* The name of a configuration object to delete.
* @param bool $delete
* Whether the previous filter allows to delete.
*
* @return bool
* TRUE to allow deletion, FALSE otherwise.
*/
public function filterDelete($name, $delete);
/**
* Filters read configuration data from the storage.
*
* @param array $names
* List of names of the configuration objects to load.
* @param array $data
* A list of the configuration data stored for the configuration object name
* that could be loaded for the passed list of names.
*
* @return array
* A list of the configuration data stored for the configuration object name
* that could be loaded for the passed list of names.
*/
public function filterReadMultiple(array $names, array $data);
/**
* Filters renaming a configuration object in the storage.
*
* @param string $name
* The name of a configuration object to rename.
* @param string $new_name
* The new name of a configuration object.
* @param bool $rename
* Allowing renaming by previous filters.
*
* @return bool
* TRUE to allow renaming, FALSE otherwise.
*/
public function filterRename($name, $new_name, $rename);
/**
* Filters what listAll should return.
*
* @param string $prefix
* The prefix to search for. If omitted, all configuration object
* names that exist are returned.
* @param array $data
* The data returned by the storage.
*
* @return array
* The filtered configuration set.
*/
public function filterListAll($prefix, array $data);
/**
* Deletes configuration objects whose names start with a given prefix.
*
* Given the following configuration object names:
* - node.type.article
* - node.type.page.
*
* Passing the prefix 'node.type.' will delete the above configuration
* objects.
*
* @param string $prefix
* The prefix to search for. If omitted, all configuration
* objects that exist will be deleted.
* @param bool $delete
* Whether to delete all or not.
*
* @return bool
* TRUE to allow deleting all, FALSE to trigger individual deletion.
*/
public function filterDeleteAll($prefix, $delete);
/**
* Allows the filter to react on creating a collection on the storage.
*
* A configuration storage can contain multiple sets of configuration objects
* in partitioned collections. The collection name identifies the current
* collection used.
*
* @param string $collection
* The collection name. Valid collection names conform to the following
* regex [a-zA-Z_.]. A storage does not need to have a collection set.
* However, if a collection is set, then the storage should use it to store
* configuration in a way that allows retrieval of configuration for a
* particular collection.
*
* @return \Drupal\config_filter\Config\StorageFilterInterface|null
* Return a filter that should participate in the collection. This allows
* filters to act on different collections. Note that a new instance of the
* filter should be created rather than returning $this directly.
*/
public function filterCreateCollection($collection);
/**
* Filter getting the existing collections.
*
* A configuration storage can contain multiple sets of configuration objects
* in partitioned collections. The collection key name identifies the current
* collection used.
*
* @param string[] $collections
* The array of existing collection names.
*
* @return array
* An array of existing collection names.
*/
public function filterGetAllCollectionNames(array $collections);
/**
* Filter the name of the current collection the storage is using.
*
* @param string $collection
* The collection found by the storage.
*
* @return string
* The current collection name.
*/
public function filterGetCollectionName($collection);
}
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Trait TransparentStorageFilterTrait.
*
* Use this trait or ConfigFilterBase to implement a config storage filter.
* Filters do not need to be plugins but it is probably the most convenient way
* to create them. Use this trait to ensure compatibility in case the interface
* ever needs to change for some reason.
*
* @package Drupal\config_filter\Config
*/
trait TransparentStorageFilterTrait {
/**
* The read-only source storage on which the filter operations are performed.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $source;
/**
* The wrapped storage which calls the filter.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $filtered;
/**
* {@inheritdoc}
*/
public function setSourceStorage(StorageInterface $storage) {
$this->source = $storage;
}
/**
* {@inheritdoc}
*/
public function setFilteredStorage(StorageInterface $storage) {
$this->filtered = $storage;
}
/**
* Get the read-only source Storage.
*
* @return \Drupal\Core\Config\StorageInterface
* The source storage.
*/
protected function getSourceStorage() {
return $this->source;
}
/**
* Get the decorator storage which applies the filters.
*
* @return \Drupal\Core\Config\StorageInterface
* The filtered decorator storage.
*/
protected function getFilteredStorage() {
return $this->filtered;
}
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterWrite($name, array $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterWriteEmptyIsDelete($name) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function filterExists($name, $exists) {
return $exists;
}
/**
* {@inheritdoc}
*/
public function filterDelete($name, $delete) {
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterRename($name, $new_name, $rename) {
return $rename;
}
/**
* {@inheritdoc}
*/
public function filterListAll($prefix, array $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterDeleteAll($prefix, $delete) {
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterCreateCollection($collection) {
return clone $this;
}
/**
* {@inheritdoc}
*/
public function filterGetAllCollectionNames(array $collections) {
return $collections;
}
/**
* {@inheritdoc}
*/
public function filterGetCollectionName($collection) {
return $collection;
}
}
<?php
namespace Drupal\config_filter;
/**
* Interface ConfigFilterManagerInterface.
*/
interface ConfigFilterManagerInterface {
/**
* Get the applicable filters for given storage names.
*
* @param string[] $storage_names
* The names of the storage plugins apply to.
* @param string[] $excluded
* The ids of filters to exclude.
*
* @return \Drupal\config_filter\Config\StorageFilterInterface[]
* The configured filter instances, keyed by filter id.
*/
public function getFiltersForStorages(array $storage_names, array $excluded = []);
/**
* Get a configured filter instance by (plugin) id.
*
* @param string $id
* The plugin id of the filter to load.
*
* @return \Drupal\config_filter\Config\StorageFilterInterface
* The ConfigFilter.
*/
public function getFilterInstance($id);
}
<?php
namespace Drupal\config_filter;
use Drupal\config_filter\Config\FilteredStorage;
use Drupal\Core\Config\StorageInterface;
/**
* Class ConfigFilterFactory.
*/
class ConfigFilterStorageFactory {
/**
* The decorated sync config storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $sync;
/**
* The filter managers to load the filters from.
*
* @var \Drupal\config_filter\ConfigFilterManagerInterface[]
*/
protected $managers = [];
/**
* ConfigFilterFactory constructor.
*
* @param \Drupal\Core\Config\StorageInterface $sync
* The original sync storage which is decorated by our filtered storage.
*/
public function __construct(StorageInterface $sync) {
$this->sync = $sync;
}
/**
* Add a config filter manager.
*
* @param \Drupal\config_filter\ConfigFilterManagerInterface $manager
* The ConfigFilter plugin manager.
*/
public function addConfigFilterManager(ConfigFilterManagerInterface $manager) {
$this->managers[] = $manager;
}
/**
* Get the sync storage Drupal uses.
*
* @return \Drupal\config_filter\Config\FilteredStorageInterface
* The decorated sync config storage.
*/
public function getSync() {
return $this->getFilteredStorage($this->sync, ['config.storage.sync']);
}
/**
* Get the sync storage Drupal uses and exclude some plugins.
*
* @param string[] $excluded
* The ids of filters to exclude.
*
* @return \Drupal\config_filter\Config\FilteredStorageInterface
* The decorated sync config storage.
*/
public function getSyncWithoutExcluded(array $excluded) {
return $this->getFilteredStorage($this->sync, ['config.storage.sync'], $excluded);
}
/**
* Get a decorated storage with filters applied.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage to decorate.
* @param string[] $storage_names
* The names of the storage, so the correct filters can be applied.
* @param string[] $excluded
* The ids of filters to exclude.
*
* @return \Drupal\config_filter\Config\FilteredStorageInterface
* The decorated storage with the filters applied.
*/
public function getFilteredStorage(StorageInterface $storage, array $storage_names, array $excluded = []) {
$filters = [];
foreach ($this->managers as $manager) {
// Filters from managers that come first will not be overwritten by
// filters from lower priority managers.
$filters = $filters + $manager->getFiltersForStorages($storage_names, $excluded);
}
return new FilteredStorage($storage, $filters);
}
}
<?php
namespace Drupal\config_filter\Exception;
use Drupal\config_filter\Config\StorageFilterInterface;
/**
* Thrown when a StorageFilterInterface is expected but not present.
*/
class InvalidStorageFilterException extends \InvalidArgumentException {
/**
* InvalidStorageFilterException constructor.
*/
public function __construct() {
parent::__construct("An argument does not implement " . StorageFilterInterface::class);
}
}
<?php
namespace Drupal\config_filter\Exception;
/**
* Thrown when calling an unsupported method on a ConfigStorage object.
*/
class UnsupportedMethod extends \BadMethodCallException {
}
<?php
namespace Drupal\config_filter\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\config_filter\Config\TransparentStorageFilterTrait;
/**
* Base class for Config filter plugin plugins.
*/
abstract class ConfigFilterBase extends PluginBase implements ConfigFilterInterface {
use TransparentStorageFilterTrait;
}
<?php
namespace Drupal\config_filter\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\config_filter\Config\StorageFilterInterface;
/**
* Defines an interface for Config filter plugin plugins.
*/
interface ConfigFilterInterface extends PluginInspectionInterface, StorageFilterInterface {
}
<?php
namespace Drupal\config_filter\Plugin;
use Drupal\config_filter\ConfigFilterManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Provides the Config filter plugin plugin manager.
*/
class ConfigFilterPluginManager extends DefaultPluginManager implements ConfigFilterManagerInterface {
/**
* Constructor for ConfigFilterPluginManager objects.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/ConfigFilter', $namespaces, $module_handler, 'Drupal\config_filter\Plugin\ConfigFilterInterface', 'Drupal\config_filter\Annotation\ConfigFilter');
$this->alterInfo('config_filter_info');
$this->setCacheBackend($cache_backend, 'config_filter_plugins');
}
/**
* {@inheritdoc}
*/
public function getFiltersForStorages(array $storage_names, array $excluded = []) {
$definitions = $this->getDefinitions();
$filters = [];
foreach ($definitions as $id => $definition) {
if ($definition['status'] && array_intersect($storage_names, $definition['storages']) && !in_array($id, $excluded)) {
$filters[$id] = $this->createInstance($id, $definition);
}
}
return $filters;
}
/**
* {@inheritdoc}
*/
public function getFilterInstance($id) {
$definitions = $this->getDefinitions();
if (array_key_exists($id, $definitions)) {
return $this->createInstance($id, $definitions[$id]);
}
return NULL;
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = array_map(function ($definition) {
if (empty($definition['storages'])) {
// The sync storage is the default.
$definition['storages'] = ['config.storage.sync'];
}
return $definition;
}, parent::findDefinitions());
// Sort the definitions by weight.
uasort($definitions, function ($a, $b) {
return strcmp($a['weight'], $b['weight']);
});
return $definitions;
}
}
<?php
namespace Drupal\config_filter\Tests;
use Drupal\config_filter\Config\GhostStorage;
use Drupal\Core\Config\StorageInterface;
use Prophecy\Argument;
/**
* Tests GhostStorage operations.
*
* @group config_filter
*/
class GhostStorageTest extends ReadonlyStorageTest {
/**
* Override the storage decorating.
*
* @param \Drupal\Core\Config\StorageInterface $source
* The storage to decorate.
*
* @return \Drupal\config_filter\Config\GhostStorage
* The storage to test.
*/
protected function getStorage(StorageInterface $source) {
return new GhostStorage($source);
}
/**
* Override the dataprovider for write methods.
*
* @dataProvider writeMethodsProvider
*/
public function testWriteOperations($method, $arguments) {
$source = $this->prophesize(StorageInterface::class);
$source->$method(Argument::any())->shouldNotBeCalled();
$storage = $this->getStorage($source->reveal());
$actual = call_user_func_array([$storage, $method], $arguments);
$this->assertTrue($actual);
}
}
<?php
namespace Drupal\config_filter\Tests;
use Drupal\config_filter\Config\ReadOnlyStorage;
use Drupal\config_filter\Exception\UnsupportedMethod;
use Drupal\Core\Config\StorageInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
/**
* Tests ReadonlyStorage operations.
*
* @group config_filter
*/
class ReadonlyStorageTest extends UnitTestCase {
/**
* Wrap a given storage.
*
* This is useful when testing a subclass of ReadonlyStorage.
*
* @param \Drupal\Core\Config\StorageInterface $source
* The storage to decorate.
*
* @return \Drupal\Core\Config\StorageInterface
* The storage wrapping the source.
*/
protected function getStorage(StorageInterface $source) {
return new ReadOnlyStorage($source);
}
/**
* Test methods that should be transparent.
*
* @dataProvider readMethodsProvider
*/
public function testReadOperations($method, $arguments, $returnValue) {
$source = $this->prophesize(StorageInterface::class);
$methodProhecy = new MethodProphecy($source, $method, $arguments);
$methodProhecy->shouldBeCalledTimes(1);
$methodProhecy->willReturn($returnValue);
$source->addMethodProphecy($methodProhecy);
$storage = $this->getStorage($source->reveal());
$actual = call_user_func_array([$storage, $method], $arguments);
$this->assertEquals($actual, $returnValue);
}
/**
* Provide the methods that should continue to work.
*
* @return array
* The data.
*/
public function readMethodsProvider() {
return [
['exists', [$this->randomMachineName()], $this->randomMachineName()],
['read', [$this->randomMachineName()], $this->randomArray()],
['readMultiple', [$this->randomArray()], $this->randomArray()],
['encode', [$this->randomArray()], $this->randomMachineName()],
['decode', [$this->randomMachineName()], $this->randomArray()],
['listAll', [$this->randomMachineName()], $this->randomArray()],
['getAllCollectionNames', [], $this->randomArray()],
['getCollectionName', [], $this->randomMachineName()],
];
}
/**
* Test creating a collection.
*
* Creating collections returns a new instance, make sure it decorates the
* new instance of the source.
*/
public function testCreateCollection() {
$name = $this->randomMachineName();
$source = $this->prophesize(StorageInterface::class);
$collectionSource = $this->prophesize(StorageInterface::class)->reveal();
$source->createCollection($name)->willReturn($collectionSource);
$storage = $this->getStorage($source->reveal());
$collectionStorage = $storage->createCollection($name);
$this->assertInstanceOf(ReadOnlyStorage::class, $collectionStorage);
$readonlyReflection = new \ReflectionClass(ReadOnlyStorage::class);
$storageProperty = $readonlyReflection->getProperty('storage');
$storageProperty->setAccessible(TRUE);
$actualSource = $storageProperty->getValue($collectionStorage);
$this->assertEquals($collectionSource, $actualSource);
}
/**
* Test the operations that should throw an error.
*
* @dataProvider writeMethodsProvider
*/
public function testWriteOperations($method, $arguments) {
$source = $this->prophesize(StorageInterface::class);
$source->$method(Argument::any())->shouldNotBeCalled();
$storage = $this->getStorage($source->reveal());
try {
call_user_func_array([$storage, $method], $arguments);
$this->fail();
}
catch (UnsupportedMethod $exception) {
$this->assertEquals(ReadOnlyStorage::class . '::' . $method . ' is not allowed on a ReadOnlyStorage', $exception->getMessage());
}
}
/**
* Provide the methods that should throw an exception.
*
* @return array
* The data
*/
public function writeMethodsProvider() {
return [
['write', [$this->randomMachineName(), $this->randomArray()]],
['delete', [$this->randomMachineName()]],
['rename', [$this->randomMachineName(), $this->randomMachineName()]],
['deleteAll', [$this->randomMachineName()]],
];
}
/**
* Get a random array.
*
* @return array
* A random array used for data testing.
*/
protected function randomArray() {
return (array) $this->getRandomGenerator()->object();
}
}
<?php
namespace Drupal\config_filter\Tests;
use Drupal\config_filter\Config\StorageFilterInterface;
use Drupal\config_filter\Plugin\ConfigFilterBase;
/**
* Class TransparentFilter.
*/
class TransparentFilter extends ConfigFilterBase implements StorageFilterInterface {
/**
* TransparentFilter constructor.
*/
public function __construct() {
parent::__construct([], 'transparent_test', []);
}
/**
* Get the read-only source Storage.
*
* @return \Drupal\Core\Config\StorageInterface
* The source storage.
*/
public function getPrivateSourceStorage() {
return $this->getSourceStorage();
}
/**
* Get the decorator storage which applies the filters.
*
* @return \Drupal\Core\Config\StorageInterface
* The filtered decorator storage.
*/
public function getPrivateFilteredStorage() {
return $this->getFilteredStorage();
}
}
{
"name": "nuvoleweb/config-filter-test-site",
"description": "Config Filter test site.",
"type": "project",
"require": {
"composer/installers": "^1.2",
"drupal-composer/drupal-scaffold": "^2.2",
"drupal/core": "~8",
"drush/drush": "~8.0",
"drupal/coder": "^8.2",
"squizlabs/php_codesniffer": "^2.8",
"phpunit/phpunit": "5.5.*",
"bovigo/assert": "~1.7",
"mikey179/vfsStream": "*"
},
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": [
{"type": "composer", "url": "https://packages.drupal.org/8"}
],
"conflict": {
"drupal/drupal": "*"
}
}
# Config Filter Split test
This is a module for testing the proper functioning of config_filter.
But it also demonstrates how a filter can be used to read and write to a
different config storage. As such it is an instructive example.
But it also has a practical purpose: We can use it to read migrate config
directly from files. Of course it is in general a bad idea to directly
edit the active configuration since Drupal does many checks and database
alterations when synchronizing configuration.
This is just here for the brave. You have been warned.
## Developing with migrate configuration read from the files.
Migrations are configurations, by default active configuration is stored in the
database. So when developing, the configuration has to be synced all the time.
To avoid this we can split out the migrate configuration to read directly
from the files.
Enable the config_filter_split_test testing module from config_filter.
To swap out the active storage, add the following to your services.local.yml:
```yaml
services:
config.storage:
class: Drupal\config_filter_split_test\Config\ActiveMigrateStorage
arguments:
- '@config.storage.active'
- '@cache.config'
- '../config/migrate_active' # The path to the folder.
```
Migrations are plugins, and plugin definitions are cached. The configuration
storage is also cached.
So either you have to clear the caches after you edit the migration or you add
the following to your settings.php (assuming you have the null cache set too):
```php
$settings['cache']['bins']['config'] = 'cache.backend.null';
$settings['cache']['bins']['discovery'] = 'cache.backend.null';
```
This has an obvious performance implication.
name: 'Simple split filter'
type: module
# core: 8.x
package: Testing
dependencies:
- config_filter:config_filter
# Information added by Drupal.org packaging script on 2018-11-14
version: '8.x-1.4'
core: '8.x'
project: 'config_filter'
datestamp: 1542184984
<?php
namespace Drupal\config_filter_migrate_test\Config;
use Drupal\config_filter\Config\FilteredStorage;
use Drupal\config_filter_split_test\Plugin\ConfigFilter\TestSplitFilter;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\CachedStorage;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\StorageInterface;
/**
* Class ActiveMigrateStorage.
*/
class ActiveMigrateStorage extends CachedStorage {
/**
* Create an ActiveMigrateStorage.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The decorated storage.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend used to store configuration.
* @param string $migrate_folder
* The migrate directory.
*/
public function __construct(StorageInterface $storage, CacheBackendInterface $cache, $migrate_folder) {
// Create the filter directly, the plugin manager is not yet available.
$filter = new TestSplitFilter(new FileStorage($migrate_folder), 'migrate_plus.migration');
// Wrap the storage with the the filtered storage.
parent::__construct(new FilteredStorage($storage, [$filter]), $cache);
}
}
<?php
namespace Drupal\config_filter_split_test\Plugin\ConfigFilter;
use Drupal\config_filter\Plugin\ConfigFilterBase;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a TestSplitFilter.
*
* This is a very basic split filter to test that config_filter applies the
* filters correctly. For more advanced and configurable split filters use the
* Configuration Split (config_split) module.
*
* @ConfigFilter(
* id = "config_filter_split_test",
* label = @Translation("Filter Split test"),
* storages = {"test_storage"},
* )
*/
class TestSplitFilter extends ConfigFilterBase implements ContainerFactoryPluginInterface {
/**
* The File storage to read the migrations from.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The name prefix to split.
*
* @var string
*/
protected $name;
/**
* Constructs a new TestSplitFilter.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The migrate storage.
* @param string $name
* The config name prefix to split.
*/
public function __construct(StorageInterface $storage, $name) {
parent::__construct([], 'config_filter_split_test', []);
$this->storage = $storage;
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(new DatabaseStorage($container->get('database'), 'config_filter_split_test'), 'core.');
}
/**
* Decide to split the config off or not.
*
* @param string $name
* The name of the configuration to check.
*
* @return bool
* Whether the configuration is supposed to be split.
*/
protected function isSplitConfig($name) {
return (strpos($name, $this->name) === 0);
}
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
if ($this->isSplitConfig($name)) {
if ($this->storage->exists($name)) {
$data = $this->storage->read($name);
}
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterExists($name, $exists) {
if ($this->isSplitConfig($name) && !$exists) {
$exists = $this->storage->exists($name);
}
return $exists;
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
return array_merge($data, $this->storage->readMultiple($names));
}
/**
* {@inheritdoc}
*/
public function filterListAll($prefix, array $data) {
return array_unique(array_merge($data, $this->storage->listAll($prefix)));
}
/**
* {@inheritdoc}
*/
public function filterWrite($name, array $data) {
if ($this->isSplitConfig($name)) {
$this->storage->write($name, $data);
return NULL;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterWriteEmptyIsDelete($name) {
return ($this->isSplitConfig($name) ? TRUE : NULL);
}
/**
* {@inheritdoc}
*/
public function filterDelete($name, $delete) {
if ($delete && $this->storage->exists($name)) {
// Call delete on the secondary storage anyway.
$this->storage->delete($name);
}
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterDeleteAll($prefix, $delete) {
if ($delete && $this->storage) {
try {
$this->storage->deleteAll($prefix);
}
catch (\UnexpectedValueException $exception) {
// The file storage tries to remove directories of collections. But this
// fails if the directory doesn't exist. So everything is actually fine.
}
}
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterCreateCollection($collection) {
return new static($this->storage->createCollection($collection), $this->name);
}
}
name: Config Filter Test
type: module
# core: 8.x
package: Testing
dependencies:
- config_filter:config_filter
# Information added by Drupal.org packaging script on 2018-11-14
version: '8.x-1.4'
core: '8.x'
project: 'config_filter'
datestamp: 1542184984
<?php
namespace Drupal\config_filter_test\plugin\ConfigFilter;
use Drupal\config_filter\Plugin\ConfigFilterBase;
/**
* Provides a pirate filter that adds "Arrr" to the site name.
*
* @ConfigFilter(
* id = "pirate_filter",
* label = "More pirates! Arrr",
* weight = 10
* )
*/
class PirateFilter extends ConfigFilterBase {
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
if ($name == 'system.site') {
$data['name'] = $data['name'] . ' Arrr';
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
if (in_array('system.site', $names)) {
$data['system.site'] = $this->filterRead('system.site', $data['system.site']);
}
return $data;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="core/tests/bootstrap.php" backupGlobals="false" colors="true" >
<php>
<ini name="error_reporting" value="32767"/>
<ini name="memory_limit" value="-1"/>
<env name="SIMPLETEST_DB" value="mysql://travis:@127.0.0.1/drupal"/>
</php>
<testsuites>
<testsuite name="tests">
<directory>./modules/config_filter/</directory>
</testsuite>
</testsuites>
</phpunit>
<?php
namespace Drupal\Tests\config_filter\Kernel;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\KernelTests\KernelTestBase;
/**
* Class ConfigFilterStorageFactoryTest.
*
* @group config_filter
*/
class ConfigFilterStorageFactoryTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'system',
'config_filter',
'config_filter_test',
'config_filter_split_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['system']);
}
/**
* Test that the config.storage.sync is decorated with the filtering version.
*/
public function testServiceProvider() {
// Export the configuration.
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
// The pirate filter changes the system.site when importing.
$this->assertEquals(['system.site'], $this->configImporter()->getStorageComparer()->getChangelist('update'));
$this->assertEmpty($this->configImporter()->getStorageComparer()->getChangelist('create'));
$this->assertEmpty($this->configImporter()->getStorageComparer()->getChangelist('delete'));
$this->assertEmpty($this->configImporter()->getStorageComparer()->getChangelist('rename'));
$config = $this->config('system.site')->getRawData();
$config['name'] .= ' Arrr';
$this->assertEquals($config, $this->container->get('config.storage.sync')->read('system.site'));
}
/**
* Test the storage factory decorating properly.
*/
public function testStorageFactory() {
/** @var \Drupal\Core\Database\Connection $database */
$database = $this->container->get('database');
$destination = new DatabaseStorage($database, 'config_filter_source_test');
// The $filtered storage will have the simple split applied to the
// destination storage, but is the unified storage.
$filtered = $this->container->get('config_filter.storage_factory')->getFilteredStorage($destination, ['test_storage']);
/** @var \Drupal\Core\Config\StorageInterface $active */
$active = $this->container->get('config.storage');
// Export the configuration to the filtered storage.
$this->copyConfig($active, $filtered);
// Get the storage of the test split plugin.
$splitStorage = new DatabaseStorage($database, 'config_filter_split_test');
// Assert that the storage is properly split.
$this->assertTrue(count($destination->listAll()) > 0);
$this->assertTrue(count($splitStorage->listAll()) > 0);
$this->assertEquals(count($active->listAll()), count($destination->listAll()) + count($splitStorage->listAll()));
$this->assertEquals($active->listAll('core'), $splitStorage->listAll());
$this->assertEquals($active->listAll('system'), $destination->listAll('system'));
$this->assertEquals($active->readMultiple($active->listAll('core')), $splitStorage->readMultiple($splitStorage->listAll()));
$this->assertEquals($active->readMultiple($active->listAll('system')), $destination->readMultiple($destination->listAll('system')));
// Reading from the $filtered storage returns the merged config.
$this->assertEquals($active->listAll(), $filtered->listAll());
$this->assertEquals($active->readMultiple($active->listAll()), $filtered->readMultiple($filtered->listAll()));
}
}
This diff is collapsed.
Config Ignore
=============
INTRODUCTION
------------
Ever experienced that your sites configuration was overridden by the configuration on the filesystem, when doing a
`drush cim`?
Not anymore!
This modules is a tool to let you keep the configuration you want, in place.
Lets say that you do would like the `system.site` configuration (which contains that sites name, slogan, email, etc) to
remain untouched, on your live site, no matter what the configuration, in the export folder says.
Or maybe you are getting tired of having the `devel.settings` changed every time you import configuration?
Then this module is what you are looking for.
REQUIREMENTS
------------
You will need the `config_filter` module to be enabled.
INSTALLATION
------------
Consult https://www.drupal.org/docs/8/extending-drupal-8/installing-contributed-modules-find-import-enable-configure-drupal-8
to see how to install and manage modules in Drupal 8.
CONFIGURATION
-------------
Go to `admin/config/development/configuration/ignore` to set what configuration you want to ignore upon import.
Do not ignore the `core.extension` configuration as it will prevent you from enabling new modules with a config import.
Use the `config_split` module for environment specific modules.
MAINTAINERS
-----------
Current maintainers:
* Tommy Lynge Jørgensen (TLyngeJ) - https://www.drupal.org/u/tlyngej
* Fabian Bircher (bircher) - https://www.drupal.org/u/bircher
{
"name": "drupal/config_ignore",
"description": "Ignore certain configuration during import.",
"type": "drupal-module",
"homepage": "http://drupal.org/project/config_ignore",
"authors": [
{
"name": "Tommy Lynge Jørgensen",
"email": "tlyngej@gmail.com",
"homepage": "https://www.drupal.org/u/tlyngej",
"role": "Maintainer"
},
{
"name": "Fabian Bircher",
"homepage": "https://www.drupal.org/u/bircher",
"role": "Maintainer"
}
],
"support": {
"issues": "http://drupal.org/project/config_ignore",
"irc": "irc://irc.freenode.org/drupal-contribute",
"source": "http://cgit.drupalcode.org/config_ignore"
},
"license": "GPL-2.0+",
"minimum-stability": "dev",
"require": {
"drupal/config_filter": "1.*"
}
}
config_ignore.settings:
type: config_object
label: 'Config Ignore Settings'
mapping:
ignored_config_entities:
type: sequence
label: 'List of ignored configurations'
sequence:
type: string
<?php
/**
* @file
* Hooks specific to the Config Ignore module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the list of config entities that should be ignored.
*/
function hook_config_ignore_settings_alter(array &$settings) {
$settings[] = 'system.site';
$settings[] = 'field.*';
}
/**
* @} End of "addtogroup hooks".
*/
name: Config Ignore
type: module
description: Ignore certain configuration during import
# core: 8.x
package: Config
configure: config_ignore.settings
dependencies:
- config_filter
# Information added by Drupal.org packaging script on 2017-10-11
version: '8.x-2.1'
core: '8.x'
project: 'config_ignore'
datestamp: 1507706047
<?php
/**
* @file
* Install, update and uninstall functions for the config_ignore module.
*/
/**
* Enable the config_filter module.
*/
function config_ignore_update_8201() {
\Drupal::getContainer()->get('module_installer')->install(['config_filter']);
}
config_ignore.settings:
route_name: 'config_ignore.settings'
title: 'Ignore'
base_route: config.sync
weight: 99
<?php
/**
* @file
* Hooks implemented by the config_ignore module.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Url;
/**
* Implements hook_form_FORM_ID_alter().
*/
function config_ignore_form_config_admin_import_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Load Services that we need.
$stock_storage_sync = \Drupal::service('config_filter.storage_factory')->getSyncWithoutExcluded(['config_ignore']);
$active_storage_sync = \Drupal::service('config.storage.sync');
$storage = \Drupal::service('config.storage');
$config_manager = \Drupal::service('config.manager');
// Create two StorageComparer objects, one with the filter enabled and one
// as without. We will compare them later to see what changes that has been
// ignored.
$unfiltered_storage_compare = new StorageComparer($stock_storage_sync, $storage, $config_manager);
$filtered_storage_compare = new StorageComparer($active_storage_sync, $storage, $config_manager);
$unfiltered_storage_compare->createChangelist();
$filtered_storage_compare->createChangelist();
// Create an array of the changes with the filter on.
$config_changes = [];
foreach ($filtered_storage_compare->getChangelist() as $config_names) {
foreach ($config_names as $config_name) {
$config_changes[] = $config_name;
}
}
foreach ($unfiltered_storage_compare->getAllCollectionNames() as $collection) {
foreach ($unfiltered_storage_compare->getChangelist(NULL, $collection) as $config_change_type => $config_names) {
foreach ($config_names as $config_name) {
// If the config name exists here, but not in the $config_changes array
// the it's because it's getting ignored.
if (!in_array($config_name, $config_changes)) {
$ignored_config_entities[] = [
$config_name,
$config_change_type,
];
}
}
}
}
// Build a table of changes that are not going to happen, due to the ignored
// config entities.
if (!empty($ignored_config_entities)) {
$form['ignored'] = [
'#type' => 'table',
'#header' => ['Config name', 'Action'],
'#caption' => t('<h3>The following configuration entities are ignored due to the <a href="@url">Config Ignore Settings</a> and therefore not displayed in the list above</h3>', [
'@url' => Url::fromRoute('config_ignore.settings')
->toString()
]),
'#rows' => $ignored_config_entities,
];
}
}
config_ignore.settings:
path: '/admin/config/development/configuration/ignore'
defaults:
_form: '\Drupal\config_ignore\Form\Settings'
_title: 'Ignore'
requirements:
_permission: 'import configuration'
<?php
namespace Drupal\config_ignore\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a setting UI for Config Ignore.
*
* @package Drupal\config_ignore\Form
*/
class Settings extends ConfigFormBase {
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'config_ignore.settings',
];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'config_ignore_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL) {
$description = $this->t('One configuration name per line.<br />
Examples: <ul>
<li>user.settings</li>
<li>views.settings</li>
<li>contact.settings</li>
<li>webform.webform.* (will ignore all config entities that starts with <em>webform.webform</em>)</li>
<li>*.contact_message.custom_contact_form.* (will ignore all config entities that starts with <em>.contact_message.custom_contact_form.</em> like fields attached to a custom contact form)</li>
<li>* (will ignore everything)</li>
<li>~webform.webform.contact (will force import for this configuration, even if ignored by a wildcard)</li>
<li>user.mail:register_no_approval_required.body (will ignore the body of the no approval required email setting, but will not ignore other user.mail configuration.)</li>
</ul>');
$config_ignore_settings = $this->config('config_ignore.settings');
$form['ignored_config_entities'] = [
'#type' => 'textarea',
'#rows' => 25,
'#title' => $this->t('Configuration entity names to ignore'),
'#description' => $description,
'#default_value' => implode(PHP_EOL, $config_ignore_settings->get('ignored_config_entities')),
'#size' => 60,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$config_ignore_settings = $this->config('config_ignore.settings');
$config_ignore_settings_array = preg_split("[\n|\r]", $values['ignored_config_entities']);
$config_ignore_settings_array = array_filter($config_ignore_settings_array);
$config_ignore_settings->set('ignored_config_entities', $config_ignore_settings_array);
$config_ignore_settings->save();
parent::submitForm($form, $form_state);
// Clear the config_filter plugin cache.
\Drupal::service('plugin.manager.config_filter')->clearCachedDefinitions();
}
}
<?php
namespace Drupal\config_ignore\Plugin\ConfigFilter;
use Drupal\Component\Utility\NestedArray;
use Drupal\config_filter\Plugin\ConfigFilterBase;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a ignore filter that reads partly from the active storage.
*
* @ConfigFilter(
* id = "config_ignore",
* label = "Config Ignore",
* weight = 100
* )
*/
class IgnoreFilter extends ConfigFilterBase implements ContainerFactoryPluginInterface {
const FORCE_EXCLUSION_PREFIX = '~';
const INCLUDE_SUFFIX = '*';
/**
* The active configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $active;
/**
* Constructs a new SplitFilter.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\StorageInterface $active
* The active configuration store with the configuration on the site.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, StorageInterface $active) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->active = $active;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
// Get the list of ignored config.
$ignored = $container->get('config.factory')->get('config_ignore.settings')->get('ignored_config_entities');
// Allow hooks to alter the list.
$container->get('module_handler')->invokeAll('config_ignore_settings_alter', [&$ignored]);
// Set the list in the plugin configuration.
$configuration['ignored'] = $ignored;
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.storage')
);
}
/**
* Match a config entity name against the list of ignored config entities.
*
* @param string $config_name
* The name of the config entity to match against all ignored entities.
*
* @return bool
* True, if the config entity is to be ignored, false otherwise.
*/
protected function matchConfigName($config_name) {
if (Settings::get('config_ignore_deactivate')) {
// Allow deactivating config_ignore in settings.php. Do not match any name
// in that case and allow a normal configuration import to happen.
return FALSE;
}
// If the string is an excluded config, don't ignore it.
if (in_array(static::FORCE_EXCLUSION_PREFIX . $config_name, $this->configuration['ignored'], TRUE)) {
return FALSE;
}
foreach ($this->configuration['ignored'] as $config_ignore_setting) {
// Split the ignore settings so that we can ignore individual keys.
$ignore = explode(':', $config_ignore_setting);
if (fnmatch($ignore[0], $config_name)) {
return TRUE;
}
}
return FALSE;
}
/**
* Read from the active configuration.
*
* This method will read the configuration from the active config store.
* But rather than just straight up returning the value it will check if
* a nested config key is set to be ignored and set only that value on the
* data to be filtered.
*
* @param string $name
* The name of the configuration to read.
* @param mixed $data
* The data to be filtered.
*
* @return mixed
* The data filtered or read from the active storage.
*/
protected function activeRead($name, $data) {
$keys = [];
foreach ($this->configuration['ignored'] as $ignored) {
// Split the ignore settings so that we can ignore individual keys.
$ignored = explode(':', $ignored);
if (fnmatch($ignored[0], $name)) {
if (count($ignored) == 1) {
// If one of the definitions does not have keys ignore the
// whole config.
return $this->active->read($name);
}
else {
// Add the sub parts to ignore to the keys.
$keys[] = $ignored[1];
}
}
}
$active = $this->active->read($name);
foreach ($keys as $key) {
$parts = explode('.', $key);
if (count($parts) == 1) {
if (isset($active[$key])) {
$data[$key] = $active[$key];
}
}
else {
$value = NestedArray::getValue($active, $parts, $key_exists);
if ($key_exists) {
// Enforce the value if it existed in the active config.
NestedArray::setValue($data, $parts, $value, TRUE);
}
}
}
return $data;
}
/**
* Read multiple from the active storage.
*
* @param array $names
* The names of the configuration to read.
* @param array $data
* The data to filter.
*
* @return array
* The new data.
*/
protected function activeReadMultiple(array $names, array $data) {
$filtered_data = [];
foreach ($names as $name) {
$filtered_data[$name] = $this->activeRead($name, $data[$name]);
}
return $filtered_data;
}
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
// Read from the active storage when the name is in the ignored list.
if ($this->matchConfigName($name)) {
return $this->activeRead($name, $data);
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterExists($name, $exists) {
// A name exists if it is ignored and exists in the active storage.
return $exists || ($this->matchConfigName($name) && $this->active->exists($name));
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
// Limit the names which are read from the active storage.
$names = array_filter($names, [$this, 'matchConfigName']);
$active_data = $this->activeReadMultiple($names, $data);
// Return the data with merged in active data.
return array_merge($data, $active_data);
}
/**
* {@inheritdoc}
*/
public function filterListAll($prefix, array $data) {
$active_names = $this->active->listAll($prefix);
// Filter out only ignored config names.
$active_names = array_filter($active_names, [$this, 'matchConfigName']);
// Return the data with the active names which are ignored merged in.
return array_unique(array_merge($data, $active_names));
}
/**
* {@inheritdoc}
*/
public function filterCreateCollection($collection) {
return new static($this->configuration, $this->pluginId, $this->pluginDefinition, $this->active->createCollection($collection));
}
/**
* {@inheritdoc}
*/
public function filterGetAllCollectionNames(array $collections) {
// Add active collection names as there could be ignored config in them.
return array_merge($collections, $this->active->getAllCollectionNames());
}
}
name: Config Ignore Hook Test
type: module
description: Module that implements all the hook from Config Ignore for testing purposes
# core: 8.x
package: Configuration
dependencies:
- config_ignore
# Information added by Drupal.org packaging script on 2017-10-11
version: '8.x-2.1'
core: '8.x'
project: 'config_ignore'
datestamp: 1507706047
<?php
/**
* @file
* Module that implements all the hook from Config Ignore for testing purposes.
*/
/**
* Implements hook_config_ignore_settings_alter().
*/
function config_ignore_hook_test_config_ignore_settings_alter(array &$settings) {
$settings[] = 'system.site';
}
<?php
namespace Drupal\Tests\config_ignore\Functional;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\StorageComparer;
use Drupal\Tests\BrowserTestBase;
/**
* Class ConfigIgnoreBrowserTestBase.
*
* @package Drupal\Tests\config_ignore
*/
abstract class ConfigIgnoreBrowserTestBase extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['config_ignore', 'config', 'config_filter'];
/**
* Perform a config import from sync. folder.
*/
public function doImport() {
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.sync'),
$this->container->get('config.storage'),
$this->container->get('config.manager')
);
$config_importer = new ConfigImporter(
$storage_comparer->createChangelist(),
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation')
);
$config_importer->reset()->import();
}
/**
* Perform a config export to sync. folder.
*/
public function doExport() {
// Setup a config sync. dir with a, more or less, know set of config
// entities. This is a full blown export of yaml files, written to the disk.
$destination = CONFIG_SYNC_DIRECTORY;
$destination_dir = config_get_config_directory($destination);
/** @var \Drupal\Core\Config\CachedStorage $source_storage */
$source_storage = \Drupal::service('config.storage');
$destination_storage = new FileStorage($destination_dir);
foreach ($source_storage->listAll() as $name) {
$destination_storage->write($name, $source_storage->read($name));
}
}
}
<?php
namespace Drupal\Tests\config_ignore\Functional;
/**
* Test hook implementation of another module.
*
* @package Drupal\Tests\config_ignore\Functional
*
* @group config_ignore
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class ConfigIgnoreHookTest extends ConfigIgnoreBrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'config_ignore',
'config',
'config_filter',
'config_ignore_hook_test'
];
/**
* Test hook implementation of another module.
*/
public function testSettingsAlterHook() {
$this->config('system.site')->set('name', 'Test import')->save();
$this->doExport();
$this->config('system.site')->set('name', 'Changed title')->save();
$this->doImport();
// Test if the `config_ignore_hook_test` module got to ignore the site name
// config.
$this->assertEquals('Changed title', $this->config('system.site')->get('name'));
}
}
<?php
namespace Drupal\Tests\config_ignore\Functional;
use Drupal\config_ignore\Plugin\ConfigFilter\IgnoreFilter;
/**
* Test functionality of config_ignore module.
*
* @package Drupal\Tests\config_ignore\Functional
*
* @group config_ignore
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class ConfigIgnoreTest extends ConfigIgnoreBrowserTestBase {
/**
* Verify that the Sync. table gets update with appropriate ignore actions.
*/
public function testSyncTableUpdate() {
$this->config('system.site')->set('name', 'Test import')->save();
$this->config('system.date')->set('first_day', '0')->save();
$this->config('config_ignore.settings')->set('ignored_config_entities', ['system.site'])->save();
$this->doExport();
// Login with a user that has permission to sync. config.
$this->drupalLogin($this->drupalCreateUser(['synchronize configuration']));
// Change the site name, which is supposed to look as an ignored change
// in on the sync. page.
$this->config('system.site')->set('name', 'Test import with changed title')->save();
$this->config('system.date')->set('first_day', '1')->save();
// Validate that the sync. table informs the user that the config will be
// ignored.
$this->drupalGet('admin/config/development/configuration');
$this->assertSession()->linkExists('Config Ignore Settings');
/** @var \Behat\Mink\Element\NodeElement[] $table_content */
$table_content = $this->xpath('//table[@id="edit-ignored"]//td');
$table_values = [];
foreach ($table_content as $item) {
$table_values[] = $item->getHtml();
}
$this->assertTrue(in_array('system.site', $table_values));
$this->assertFalse(in_array('system.date', $table_values));
}
/**
* Verify that the settings form works.
*/
public function testSettingsForm() {
// Login with a user that has permission to import config.
$this->drupalLogin($this->drupalCreateUser(['import configuration']));
$edit = [
'ignored_config_entities' => 'config.test',
];
$this->drupalGet('admin/config/development/configuration/ignore');
$this->submitForm($edit, t('Save configuration'));
$settings = $this->config('config_ignore.settings')->get('ignored_config_entities');
$this->assertEqual($settings, ['config.test']);
}
/**
* Verify that config can get ignored.
*/
public function testValidateIgnoring() {
// Set the site name to a known value that we later will try and overwrite.
$this->config('system.site')->set('name', 'Test import')->save();
// Set the system.site:name to be ignored upon config import.
$this->config('config_ignore.settings')->set('ignored_config_entities', ['system.site'])->save();
$this->doExport();
// Change the site name, perform an import and see if the site name remains
// the same, as it should.
$this->config('system.site')->set('name', 'Changed title')->save();
$this->doImport();
$this->assertEquals('Changed title', $this->config('system.site')->get('name'));
}
/**
* Verify all wildcard asterisk is working.
*/
public function testValidateIgnoringWithWildcard() {
// Set the site name to a known value that we later will try and overwrite.
$this->config('system.site')->set('name', 'Test import')->save();
// Set the system.site:name to be ignored upon config import.
$this->config('config_ignore.settings')->set('ignored_config_entities', ['system.' . IgnoreFilter::INCLUDE_SUFFIX])->save();
$this->doExport();
// Change the site name, perform an import and see if the site name remains
// the same, as it should.
$this->config('system.site')->set('name', 'Changed title')->save();
$this->doImport();
$this->assertEquals('Changed title', $this->config('system.site')->get('name'));
}
/**
* Verify Force Import syntax is working.
*
* This test makes sure we avoid regression issues.
*/
public function testValidateForceImporting() {
// Set the site name to a known value that we later will try and overwrite.
$this->config('system.site')->set('name', 'Test import')->save();
// Set the system.site:name to be (force-) imported upon config import.
$settings = [IgnoreFilter::FORCE_EXCLUSION_PREFIX . 'system.site'];
$this->config('config_ignore.settings')->set('ignored_config_entities', $settings)->save();
$this->doExport();
// Change the site name, perform an import and see if the site name remains
// the same, as it should.
$this->config('system.site')->set('name', 'Changed title')->save();
$this->doImport();
$this->assertEquals('Test import', $this->config('system.site')->get('name'));
}
/**
* Verify excluded configuration works with wildcards.
*
* This test cover the scenario where a wildcard matches a specific
* configuration, but that's still imported due exclusion.
*/
public function testValidateForceImportingWithWildcard() {
// Set the site name to a known value that we later will try and overwrite.
$this->config('system.site')->set('name', 'Test import')->save();
// Set the system.site:name to be (force-) imported upon config import.
$settings = ['system.' . IgnoreFilter::INCLUDE_SUFFIX, IgnoreFilter::FORCE_EXCLUSION_PREFIX . 'system.site'];
$this->config('config_ignore.settings')->set('ignored_config_entities', $settings)->save();
$this->doExport();
// Change the site name, perform an import and see if the site name remains
// the same, as it should.
$this->config('system.site')->set('name', 'Changed title')->save();
$this->doImport();
$this->assertEquals('Test import', $this->config('system.site')->get('name'));
}
/**
* Verify ignoring only some config keys.
*
* This test covers the scenario when not the whole config is to be ignored
* but only a certain subset of it.
*/
public function testValidateImportingWithIgnoredSubKeys() {
// Set the site name to a known value that we later will try and overwrite.
$this->config('system.site')
->set('name', 'Test name')
->set('slogan', 'Test slogan')
->set('page.front', '/ignore')
->save();
// Set the system.site:name to be (force-) imported upon config import.
$settings = ['system.site:name', 'system.site:page.front'];
$this->config('config_ignore.settings')->set('ignored_config_entities', $settings)->save();
$this->doExport();
// Change the site name, perform an import and see if the site name remains
// the same, as it should.
$this->config('system.site')
->set('name', 'Changed title')
->set('slogan', 'Changed slogan')
->set('page.front', '/new-ignore')
->save();
$this->doImport();
$this->assertEquals('Changed title', $this->config('system.site')->get('name'));
$this->assertEquals('Test slogan', $this->config('system.site')->get('slogan'));
$this->assertEquals('/new-ignore', $this->config('system.site')->get('page.front'));
}
}
This diff is collapsed.
# Configuration split
## Background
The Drupal 8 configuration management works best when importing and exporting
the whole set of the sites configuration. However, sometimes developers like to
opt out of the robustness of CM and have a super-set of configuration active on
their development machine. The canonical example for this is to have the
<code>devel</code> module enabled or having a few block placements or views in
the development environment and then not export them into the set of
configuration to be deployed, yet still being able to share the development
configuration with colleagues.
This module allows to define sets of configuration that will get exported to
separate directories when exporting, and get merged together when importing.
It is possible to define in settings.php which of these sets should be active
and considered for the export and import.
## How to use it.
Let us assume that you configured your sync directory as follows:
```php
$config_directories[CONFIG_SYNC_DIRECTORY] = '../config/sync';
```
Create a split with the folder `../config/my-split-folder` and create that
directory. Now add a module that is currently active that you wish not to
export, say `devel`. Next export all the configuration (with `drush cex` for
drush >= 8.1.10 and `drush csex` for older versions of drush).
This should have removed devel from `core.extensions` and moved the devel
configuration to the split folder.
Next you can disable the split in the UI and enable it with a config override.
```php
// In settings.php assuming your split was called 'my_split'
$config['config_split.config_split.my_split']['status'] = TRUE;
```
Now export the configuration again and you will see the split being deactivated
But it is still active on your development site due to the override.
Now deploy the configuration and devel will be un-installed.
On another developers machine just import the configuration, add the override,
clear the cache, and import again to have devel enabled on that environment.
You should only edit active splits as inactive splits will not take effect when
exporting the configuration.
NOTE: Do **NOT** put configuration directories inside of each other.
In particular the split folder **MUST NOT** be inside of the sync directory.
Recommended is a sibling, or in other words a folder that shares the same
parent as the sync directory or in a folder with other split folders which
is next to the sync folder.
Examples:
```
../config/
├── dev
├── sync
└── test
../config/
├── sync
└── splits
├── dev
└── live
```
## How it works
The module depends on Config Filter for the integration with the import/export
pipeline of the Drupal UI and drush. The configuration is read from the main
directory and also from split directories under the hood. Presenting Drupal
with the unified configuration of the sync directory and the extra
configuration defined in splits. Importing and exporting works the same way as
before, except some configuration is read from and written to different
directories. Importing configuration still removes configuration not present in
the files. Thus, the robustness and predictability of the configuration
management remains.
## Attention
You may need to single-import the split configuration if it changes apart from
which extensions are split off.
And remember to clear the caches when overriding the splits configuration.
## Notice
The important part to remember is to use Drupal 8s configuration management the
way it was intended to be used. This module does not interfere with the active
configuration but instead filters on the import/export pipeline. If you use
this module you should have a staging environment where you can let the
configuration management do its job and verify that everything is good for
deployment.
{
"name": "drupal/config_split",
"type": "drupal-module",
"description": "Configuration filter for importing and exporting extra config",
"keywords": ["Drupal", "configuration", "configuration management"],
"authors": [
{
"name": "Fabian Bircher",
"email": "opensource@fabianbircher.com",
"homepage": "https://www.drupal.org/u/bircher",
"role": "Maintainer"
},
{
"name": "Nuvole Web",
"email": "info@nuvole.org",
"homepage": "http://nuvole.org",
"role": "Maintainer"
}
],
"homepage": "https://www.drupal.org/project/config_split",
"support": {
"issues": "https://www.drupal.org/project/issues/config_split",
"irc": "irc://irc.freenode.org/drupal-contribute",
"source": "http://cgit.drupalcode.org/config_split"
},
"license": "GPL-2.0+",
"require": {
"drupal/config_filter": "*"
},
"extra": {
"drush": {
"services": {
"drush.services.yml": "^9"
}
}
}
}
config_split.config_split.*:
type: config_entity
label: 'Configuration Split Setting'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
description:
type: label
label: 'Description'
uuid:
type: string
weight:
type: integer
label: 'Weight'
status:
type: boolean
label: 'Active'
folder:
type: string
label: 'Folder'
module:
type: sequence
label: 'Filtered modules'
sequence:
type: integer
label: 'Weight'
theme:
type: sequence
label: 'Filtered themes'
sequence:
type: integer
label: 'Weight'
blacklist:
type: sequence
label: 'Filtered configuration'
sequence:
type: string
graylist:
type: sequence
label: 'Ignored configuration'
sequence:
type: string
graylist_dependents:
type: boolean
label: 'Include dependent configuration'
graylist_skip_equal:
type: boolean
label: 'Skip graylisted config without a change.'
<?php
/**
* @file
* Drush integration for the config_split module.
*/
use Drush\Log\LogLevel;
/**
* Implements hook_drush_command().
*/
function config_split_drush_command() {
$items['config-split-export'] = [
'description' => 'Export only split configuration to a directory.',
'core' => ['8+'],
'aliases' => ['csex'],
'arguments' => [
'split' => 'The split configuration to export, if none is given do a normal export.',
],
'options' => [],
'examples' => [
'drush config-split-export development' => 'Export development configuration; assumes a "development" split, export only that.',
],
];
$items['config-split-import'] = [
'description' => 'Import only config from a split.',
'core' => ['8+'],
'aliases' => ['csim'],
'arguments' => [
'split' => 'The split configuration to export, if none is given do a normal import.',
],
'options' => [],
'examples' => [
'drush config-split-import' => 'Import configuration as drush cim does.',
],
];
return $items;
}
/**
* Command callback: Export config to specified directory (usually sync).
*/
function drush_config_split_export($split = NULL) {
try {
// Make the magic happen.
\Drupal::service('config_split.cli')->ioExport($split, new ConfigSplitDrush8Io(), 'dt');
}
catch (Exception $e) {
return drush_set_error('DRUSH_CONFIG_ERROR', $e->getMessage());
}
}
/**
* Command callback. Import from specified config directory (defaults to sync).
*/
function drush_config_split_import($split = NULL) {
try {
// Make the magic happen.
\Drupal::service('config_split.cli')->ioImport($split, new ConfigSplitDrush8Io(), 'dt');
}
catch (Exception $e) {
return drush_set_error('DRUSH_CONFIG_ERROR', $e->getMessage());
}
}
// @codingStandardsIgnoreStart
/**
* Class ConfigSplitDrush8Io.
*
* This is a stand in for \Symfony\Component\Console\Style\StyleInterface with
* drush 8 so that we don't need to depend on symfony components.
*/
class ConfigSplitDrush8Io {
public function confirm($text) {
return drush_confirm($text);
}
public function success($text) {
drush_log($text, LogLevel::SUCCESS);
}
public function error($text) {
drush_log($text, LogLevel::ERROR);
}
public function text($text) {
drush_log($text, LogLevel::NOTICE);
}
}
// @codingStandardsIgnoreEnd
name: Configuration split
type: module
description: Configuration filter for importing and exporting split config
# core: 8.x
package: Config
configure: entity.config_split.collection
dependencies:
- config_filter:config_filter
# Information added by Drupal.org packaging script on 2018-09-26
version: '8.x-1.4'
core: '8.x'
project: 'config_split'
datestamp: 1537971797
<?php
/**
* @file
* Install, update and uninstall functions for the config_split module.
*/
/**
* Enable the config_filter module.
*/
function config_split_update_8001() {
\Drupal::getContainer()->get('module_installer')->install(['config_filter']);
}
entity.config_split.add_form:
route_name: 'entity.config_split.add_form'
title: 'Add Configuration Split Setting'
appears_on:
- entity.config_split.collection
# Configuration Split Setting menu items definition
entity.config_split.collection:
title: 'Configuration Split Settings'
description: 'List Configuration Split settings'
route_name: entity.config_split.collection
parent: system.admin_config_development
<?php
/**
* @file
* Configuration split module functions.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_form_FORM_ID_alter().
*/
function config_split_form_config_admin_import_form_alter(&$form, FormStateInterface $form_state) {
$enabled = [];
$used_config_split_text = t('You are not using any config split configuration');
$config_split_entities = \Drupal::entityTypeManager()->getStorage('config_split')->loadMultiple();
$active_filters = \Drupal::service('plugin.manager.config_filter')->getDefinitions();
$active_filters = array_filter($active_filters, function ($filter) {
return $filter['status'];
});
/** @var \Drupal\config_split\Entity\ConfigSplitEntityInterface $config_split_entity */
foreach ($config_split_entities as $config_split_entity) {
if (in_array('config_split:' . $config_split_entity->id(), array_keys($active_filters))) {
$enabled[] = Link::fromTextAndUrl($config_split_entity->label(), $config_split_entity->toUrl())->toString();
// Read the configuration and check differences in important fields.
$config_name = $config_split_entity->getConfigDependencyName();
$active = \Drupal::getContainer()->get('config.storage')->read($config_name);
$staged = \Drupal::getContainer()->get('config.storage.sync')->read($config_name);
$fields = ['status', 'weight', 'folder'];
$warnings = array_sum(array_map(function ($filed) use ($active, $staged) {
return $active[$filed] != $staged[$filed];
}, $fields));
if ($warnings) {
\Drupal::messenger()
->addWarning(t('The configuration for @split has changed, consider a single import for it first.', ['@split' => $config_split_entity->label()]));
}
}
}
if (!empty($enabled)) {
$used_config_split_text = t('Used config split configuration:') . ' ' . implode(', ', $enabled);
}
$form['config_split']['#weight'] = -10;
$form['config_split']['#markup'] = '<p>' . $used_config_split_text . '</p>';
}
/**
* Implements hook_help().
*/
function config_split_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.config_split':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('
The Drupal 8 configuration management works best when importing and exporting
the whole set of the sites configuration. However, sometimes developers like to
opt out of the robustness of CM and have a super-set of configuration active on
their development machine. The canonical example for this is to have the
<code>devel</code> module enabled or having a few block placements or views in
the development environment and then not export them into the set of
configuration to be deployed, yet still being able to share the development
configuration with colleagues.
This module allows to define sets of configuration that will get exported to
separate directories when exporting, and get merged together when importing.
It is possible to define in settings.php which of these sets should be active
and considered for the export and import.
For more information, see the <a href=":online-documentation">online documentation for the Config Split module</a>.',
[
':online-documentation' => 'https://www.drupal.org/docs/8/modules/configuration-split',
])
. '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Setting up a split') . '</dt>';
$output .= '<dd>' . t('
Let us assume that you configured your sync directory as follows:
<code>
$config_directories[CONFIG_SYNC_DIRECTORY] = \'../config/sync\';
</code>
Create a split with the folder `../config/my-split-folder` and create that
directory. Now add a module that is currently active that you wish not to
export, say `devel`. Next export all the configuration (with `drush cex` for
drush >= 8.1.10 and `drush csex` for older versions of drush).
This should have removed devel from `core.extensions` and moved the devel
configuration to the split folder.') . '</dd>';
$output .= '<dt>' . t('Deploy splits') . '</dt>';
$output .= '<dd>' . t('
Now deploy the configuration and devel will be un-installed.
On another developers machine just import the configuration, add the override,
clear the cache, and import again to have devel enabled on that environment.
You should only edit active splits as inactive splits will not take effect when
exporting the configuration.') . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.config_split.add_form':
case 'entity.config_split.edit_form':
$output = '';
$output .= '<p>' . t('Config Split Help') . '</p>';
$output .= '<p>' . t('The configuration listed as part of a split
are exported to the split directory rather than the usual sync directory
when exporting the whole configuration. When importing the whole
configuration, the configuration in the split directories is merged with
the default sync directory and overrides the configuration.
The configuration does not end up being overwritten in the sense of
configuration overrides such as the overrides from settings.php.') . '</p>';
return $output;
}
return NULL;
}
administer configuration split:
title: 'Administer configuration split'
restrict access: true
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
description: 'Configuration Split Export'
options: {}
arguments: {}
messages:
success: 'Configuration successfully exported.'
directories: 'The following directories will be purged and used for exporting configuration:'
question: 'Export the configuration?'
split_destination_error: 'The split-destination can only be used with one split, consider overriding the config in settings.php if you wand different paths.'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment