added features

This commit is contained in:
Bachir Soussi Chiadmi
2016-09-06 12:46:13 +02:00
parent f192bc2ffa
commit c6cff46234
109 changed files with 17546 additions and 0 deletions
@@ -0,0 +1,351 @@
<?php
namespace Drupal\features;
/**
* Contains some configuration together with metadata like the name + package.
*
* @todo Should the object be immutable?
* @todo Should this object have an interface?
*/
class ConfigurationItem {
/**
* Prefixed configuration item name.
*
* @var string
*/
protected $name;
/**
* Configuration item name without prefix.
*
* @var string
*/
protected $shortName;
/**
* Human readable name of configuration item.
*
* @var string
*/
protected $label;
/**
* Type of configuration.
*
* @var string
*/
protected $type;
/**
* The contents of the configuration item in exported format.
*
* @var array
*/
protected $data;
/**
* Array of names of dependent configuration items.
*
* @var string[]
*/
protected $dependents = [];
/**
* Feature subdirectory to export item to.
*
* @var string
*/
protected $subdirectory;
/**
* Machine name of a package the configuration is assigned to.
*
* @var string
*/
protected $package;
/**
* Whether the configuration is marked as excluded.
*
* @var bool
*/
protected $excluded = FALSE;
/**
* Whether the configuration provider is excluded.
*
* @var bool
*/
protected $providerExcluded = FALSE;
/**
* The provider of the config item.
*
* @var string
*/
protected $provider;
/**
* Array of package names that this item should be excluded from.
*
* @var string[]
*/
protected $packageExcluded = [];
/**
* Creates a new ConfigurationItem instance.
*
* @param string $name
* The config name.
* @param array $data
* The config data.
* @param array $additional_properties
* (optional) Additional properties set on the object.
*/
public function __construct($name, array $data, array $additional_properties = []) {
$this->name = $name;
$this->data = $data;
$properties = get_object_vars($this);
foreach ($additional_properties as $property => $value) {
if (!array_key_exists($property, $properties)) {
throw new \InvalidArgumentException('Invalid property: ' . $property);
}
$this->{$property} = $value;
}
}
/**
* Calculates the config type usable in configuration.
*
* By default Drupal uses system.simple as config type, which cannot be used
* inside configuration itself. Therefore convert it to system_simple.
*
* @param string $type
* The config type provided by core.
*
* @return string
* The config type as string without dots.
*/
public static function fromConfigTypeToConfigString($type) {
return $type == 'system.simple' ? FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG : $type;
}
/**
* Converts a config type string in configuration back to the config type.
*
* @param string $type
* The config type as string without dots.
*
* @return string
* The config type provided by core.
*/
public static function fromConfigStringToConfigType($type) {
return $type == FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG ? 'system.simple' : $type;
}
/**
* @return mixed
*/
public function getName() {
return $this->name;
}
/**
* @param mixed $name
*
* @return ConfigurationItem
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* @return mixed
*/
public function getShortName() {
return $this->shortName;
}
/**
* @param mixed $shortName
*
* @return ConfigurationItem
*/
public function setShortName($shortName) {
$this->shortName = $shortName;
return $this;
}
/**
* @return mixed
*/
public function getLabel() {
return $this->label;
}
/**
* @param mixed $label
*
* @return ConfigurationItem
*/
public function setLabel($label) {
$this->label = $label;
return $this;
}
/**
* @return mixed
*/
public function getType() {
return $this->type;
}
/**
* @param mixed $type
*
* @return ConfigurationItem
*/
public function setType($type) {
$this->type = $type;
return $this;
}
/**
* @return mixed
*/
public function getData() {
return $this->data;
}
/**
* @param mixed array
*
* @return ConfigurationItem
*/
public function setData(array $data) {
$this->data = $data;
return $this;
}
/**
* @return string[]
*/
public function getDependents() {
return $this->dependents;
}
/**
* @param array $dependents
*
* @return ConfigurationItem
*/
public function setDependents($dependents) {
$this->dependents = $dependents;
return $this;
}
/**
* @return mixed
*/
public function getSubdirectory() {
return $this->subdirectory;
}
/**
* @param mixed $subdirectory
*
* @return ConfigurationItem
*/
public function setSubdirectory($subdirectory) {
$this->subdirectory = $subdirectory;
return $this;
}
/**
* @return mixed
*/
public function getPackage() {
return $this->package;
}
/**
* @param mixed $package
*
* @return ConfigurationItem
*/
public function setPackage($package) {
$this->package = $package;
return $this;
}
/**
* @return boolean
*/
public function isExcluded() {
return $this->excluded;
}
/**
* @param boolean $excluded
*
* @return ConfigurationItem
*/
public function setExcluded($excluded) {
$this->excluded = $excluded;
return $this;
}
/**
* @return boolean
*/
public function isProviderExcluded() {
return $this->providerExcluded;
}
/**
* @param boolean $providerExcluded
*
* @return ConfigurationItem
*/
public function setProviderExcluded($providerExcluded) {
$this->providerExcluded = $providerExcluded;
return $this;
}
/**
* @return string
*/
public function getProvider() {
return $this->provider;
}
/**
* @param string $provider
*/
public function setProvider($provider) {
$this->provider = $provider;
return $this;
}
/**
* @return string[]
*/
public function getPackageExcluded() {
return $this->packageExcluded;
}
/**
* @param array $packageExcluded
*
* @return ConfigurationItem
*/
public function setPackageExcluded($packageExcluded) {
$this->packageExcluded = $packageExcluded;
return $this;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\features\Controller;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\system\FileDownloadController;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Returns responses for config module routes.
*/
class FeaturesController implements ContainerInjectionInterface {
/**
* The file download controller.
*
* @var \Drupal\system\FileDownloadController
*/
protected $fileDownloadController;
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
new FileDownloadController(),
$container->get('csrf_token')
);
}
/**
* Constructs a FeaturesController object.
*
* @param \Drupal\system\FileDownloadController $file_download_controller
* The file download controller.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
public function __construct(FileDownloadController $file_download_controller, CsrfTokenGenerator $csrf_token) {
$this->fileDownloadController = $file_download_controller;
$this->csrfToken = $csrf_token;
}
/**
* Downloads a tarball of the site configuration.
*
* @param string $uri
* The URI to download.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* The downloaded file.
*/
public function downloadExport($uri, Request $request) {
if ($uri) {
// @todo Simplify once https://www.drupal.org/node/2630920 is solved.
if (!$this->csrfToken->validate($request->query->get('token'), $uri)) {
throw new AccessDeniedHttpException();
}
$request = new Request(array('file' => $uri));
return $this->fileDownloadController->download($request, 'temporary');
}
}
}
@@ -0,0 +1,305 @@
<?php
namespace Drupal\features\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\features\FeaturesAssignmentMethodInterface;
use Drupal\features\FeaturesBundleInterface;
/**
* Defines a features bundle.
* @todo Better description
*
* @ConfigEntityType(
* id = "features_bundle",
* label = @Translation("Features bundle"),
* handlers = {
* },
* admin_permission = "administer site configuration",
* config_prefix = "bundle",
* entity_keys = {
* "id" = "machine_name",
* "label" = "name"
* },
* links = {
* },
* config_export = {
* "name",
* "machine_name",
* "description",
* "assignments",
* "profile_name",
* "is_profile",
* }
* )
*/
class FeaturesBundle extends ConfigEntityBase implements FeaturesBundleInterface {
/**
* @var string
*/
protected $name;
/**
* @var
*/
protected $machine_name;
/**
* @var string
*/
protected $description;
/**
* @var string[]
*/
protected $assignments = [];
/**
* @var string
*/
protected $profile_name;
/**
* @var bool
*/
protected $is_profile;
public function id() {
// @todo Convert it to $this->id in the long run.
return $this->getMachineName();
}
/**
* {@inheritdoc}
*/
public function isDefault() {
return $this->machine_name == static::DEFAULT_BUNDLE;
}
/**
* {@inheritdoc}
*/
public function getMachineName() {
return $this->machine_name;
}
/**
* {@inheritdoc}
*/
public function setMachineName($machine_name) {
$this->machine_name = $machine_name;
}
/**
* {@inheritdoc}
*/
public function getName() {
return $this->name;
}
/**
* {@inheritdoc}
*/
public function setName($name) {
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public function getFullName($short_name) {
if ($this->isDefault() || $this->inBundle($short_name)) {
return $short_name;
}
else {
return $this->machine_name . '_' . $short_name;
}
}
/**
* {@inheritdoc}
*/
public function getShortName($machine_name) {
if (!$this->isProfilePackage($machine_name) && $this->inBundle($machine_name)) {
return substr($machine_name, strlen($this->getMachineName()) + 1, strlen($machine_name) - strlen($this->getMachineName()) - 1);
}
return $machine_name;
}
/**
* {@inheritdoc}
*/
public function inBundle($machine_name) {
return ($this->isProfilePackage($machine_name) || strpos($machine_name, $this->machine_name . '_') === 0);
}
/**
* {@inheritdoc}
*/
public function isProfilePackage($machine_name) {
return ($this->isProfile() && $machine_name == $this->getProfileName());
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public function isProfile() {
return $this->is_profile;
}
/**
* {@inheritdoc}
*/
public function setIsProfile($value) {
$this->is_profile = $value;
}
/**
* {@inheritdoc}
*/
public function getProfileName() {
$name = $this->isProfile() ? $this->profile_name : '';
return !empty($name) ? $name : drupal_get_profile();
}
/**
* {@inheritdoc}
*/
public function setProfileName($machine_name) {
$this->profile_name = $machine_name;
}
/**
* {@inheritdoc}
*/
public function getEnabledAssignments() {
$list = array();
foreach ($this->assignments as $method_id => $method) {
if ($method['enabled']) {
$list[$method_id] = $method_id;
}
}
return $list;
}
/**
* {@inheritdoc}
*/
public function setEnabledAssignments(array $assignments) {
// Add any new assignments that we don't yet know about.
$new_assignments = array_diff($assignments, array_keys($this->assignments));
foreach ($new_assignments as $method_id) {
$this->assignments[$method_id] = $this->getAssignmentSettings($method_id);
}
foreach ($this->assignments as $method_id => &$method) {
$method['enabled'] = in_array($method_id, $assignments);
}
}
/**
* {@inheritdoc}
*/
public function getAssignmentWeights() {
$list = array();
foreach ($this->assignments as $method_id => $method) {
$list[$method_id] = $method['weight'];
}
return $list;
}
/**
* {@inheritdoc}
*/
public function setAssignmentWeights(array $assignments) {
foreach ($this->assignments as $method_id => &$method) {
if (isset($assignments[$method_id])) {
$method['weight'] = $assignments[$method_id];
}
}
}
/**
* Return array of default settings for the given plugin method
*
* @param $method_id
* @return array
*/
protected function getDefaultSettings($method_id) {
$settings = ['enabled' => FALSE, 'weight' => 0];
$manager = \Drupal::service('plugin.manager.features_assignment_method');
$definition = $manager->getDefinition($method_id);
if (isset($definition['weight'])) {
$settings['weight'] = $definition['weight'];
}
if (isset($definition['default_settings'])) {
$settings += $definition['default_settings'];
}
return $settings;
}
/**
* {@inheritdoc}
*/
public function getAssignmentSettings($method_id = NULL) {
if (isset($method_id)) {
if (isset($this->assignments[$method_id])) {
return $this->assignments[$method_id];
}
else {
// Use defaults.
return $this->getDefaultSettings($method_id);
}
}
else {
$list = array();
foreach (array_keys($this->assignments) as $method_id) {
$list[$method_id] = $this->getAssignmentSettings($method_id);
}
return $list;
}
}
/**
* {@inheritdoc}
*/
public function setAssignmentSettings($method_id, array $settings) {
if (isset($method_id)) {
$this->assignments[$method_id] = $settings;
}
else {
foreach ($settings as $method_id => $method_settings) {
if (!empty($method_settings)) {
$this->setAssignmentSettings($method_id, $method_settings);
}
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function remove() {
$this->delete();
}
}
@@ -0,0 +1,446 @@
<?php
namespace Drupal\features;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ExtensionInstallStorage;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\features\Entity\FeaturesBundle;
/**
* Class responsible for performing package assignment.
*/
class FeaturesAssigner implements FeaturesAssignerInterface {
use StringTranslationTrait;
/**
* The package assignment method plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $assignerManager;
/**
* The features manager.
*
* @var \Drupal\features\FeaturesManagerInterface
*/
protected $featuresManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Local cache for package assignment method instances.
*
* @var array
*/
protected $methods;
/**
* Bundles.
*
* @var array of \Drupal\features\FeaturesBundleInterface
*/
protected $bundles;
/**
* Currently active bundle.
*
* @var \Drupal\features\FeaturesBundleInterface
*/
protected $currentBundle;
/**
* Constructs a new FeaturesAssigner object.
*
* @param \Drupal\features\FeaturesManagerInterface $features_manager
* The features manager.
* @param \Drupal\Component\Plugin\PluginManagerInterface $assigner_manager
* The package assignment methods plugin manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The configuration factory.
*/
public function __construct(FeaturesManagerInterface $features_manager, PluginManagerInterface $assigner_manager, EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, StorageInterface $config_storage) {
$this->featuresManager = $features_manager;
$this->assignerManager = $assigner_manager;
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
$this->configStorage = $config_storage;
$this->bundles = $this->getBundleList();
$this->currentBundle = $this->getBundle(FeaturesBundleInterface::DEFAULT_BUNDLE);
// Ensure bundle information is fresh.
$this->createBundlesFromPackages();
}
/**
* Initializes the injected features manager with the assigner.
*
* This should be called right after instantiating the assigner to make it
* available to the features manager without introducing a circular
* dependency.
*/
public function initFeaturesManager() {
$this->featuresManager->setAssigner($this);
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->methods = array();
$this->featuresManager->reset();
}
/**
* Gets enabled assignment methods.
*
* @return array
* An array of enabled assignment methods, sorted by weight.
*/
public function getEnabledAssigners() {
$enabled = $this->currentBundle->getEnabledAssignments();
$weights = $this->currentBundle->getAssignmentWeights();
foreach ($enabled as $key => $value) {
$enabled[$key] = $weights[$key];
}
asort($enabled);
return $enabled;
}
/**
* Clean up the package list after all config has been assigned
*/
protected function cleanup() {
$packages = $this->featuresManager->getPackages();
foreach ($packages as $index => $package) {
if ($package->getStatus() === FeaturesManagerInterface::STATUS_NO_EXPORT && empty($package->getConfig()) && empty($package->getConfigOrig())) {
unset($packages[$index]);
}
}
$this->featuresManager->setPackages($packages);
}
/**
* {@inheritdoc}
*/
public function assignConfigPackages($force = FALSE) {
foreach ($this->getEnabledAssigners() as $method_id => $info) {
$this->applyAssignmentMethod($method_id, $force);
}
$this->cleanup();
}
/**
* {@inheritdoc}
*/
public function applyAssignmentMethod($method_id, $force = FALSE) {
$this->getAssignmentMethodInstance($method_id)->assignPackages($force);
}
/**
* {@inheritdoc}
*/
public function getAssignmentMethods() {
return $this->assignerManager->getDefinitions();
}
/**
* Returns an instance of the specified package assignment method.
*
* @param string $method_id
* The string identifier of the package assignment method to use to package
* configuration.
*
* @return \Drupal\features\FeaturesAssignmentMethodInterface
*/
protected function getAssignmentMethodInstance($method_id) {
if (!isset($this->methods[$method_id])) {
$instance = $this->assignerManager->createInstance($method_id, array());
$instance->setFeaturesManager($this->featuresManager);
$instance->setAssigner($this);
$instance->setEntityManager($this->entityManager);
$instance->setConfigFactory($this->configFactory);
$this->methods[$method_id] = $instance;
}
return $this->methods[$method_id];
}
/**
* {@inheritdoc}
*/
public function purgeConfiguration() {
// Ensure that we are getting the defined package assignment information.
// An invocation of \Drupal\Core\Extension\ModuleHandler::install() or
// \Drupal\Core\Extension\ModuleHandler::uninstall() could invalidate the
// cached information.
$this->assignerManager->clearCachedDefinitions();
$this->featuresManager->reset();
}
/**
* {@inheritdoc}
*/
public function getBundle($name = NULL) {
if (empty($name)) {
return $this->currentBundle;
}
elseif (isset($this->bundles[$name])) {
return $this->bundles[$name];
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function setBundle(FeaturesBundleInterface $bundle, $current = TRUE) {
$this->bundles[$bundle->getMachineName()] = $bundle;
if (isset($this->currentBundle) && ($current || ($bundle->getMachineName() == $this->currentBundle->getMachineName()))) {
$this->currentBundle = $bundle;
}
}
/**
* {@inheritdoc}
*/
public function findBundle(array $info, $features_info = NULL) {
$bundle = NULL;
if (!empty($features_info['bundle'])) {
$bundle = $this->getBundle($features_info['bundle']);
}
elseif (!empty($info['package'])) {
$bundle = $this->findBundleByName($info['package']);
}
if (!isset($bundle)) {
// Return the default bundle.
return $this->getBundle(FeaturesBundleInterface::DEFAULT_BUNDLE);
}
return $bundle;
}
/**
* {@inheritdoc}
*/
public function setCurrent(FeaturesBundleInterface $bundle) {
$this->currentBundle = $bundle;
$session = \Drupal::request()->getSession();
if (isset($session)) {
$session->set('features_current_bundle', $bundle->getMachineName());
}
return $bundle;
}
/**
* {@inheritdoc}
*/
public function getBundleList() {
if (empty($this->bundles)) {
$this->bundles = array();
foreach ($this->entityManager->getStorage('features_bundle')->loadMultiple() as $machine_name => $bundle) {
$this->bundles[$machine_name] = $bundle;
}
}
return $this->bundles;
}
/**
* {@inheritdoc}
*/
public function findBundleByName($name, $create = FALSE) {
$bundles = $this->getBundleList();
foreach ($bundles as $machine_name => $bundle) {
if ($name == $bundle->getName()) {
return $bundle;
}
}
$machine_name = strtolower(str_replace(array(' ', '-'), '_', $name));
if (isset($bundles[$machine_name])) {
return $bundles[$machine_name];
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function createBundleFromDefault($machine_name, $name = NULL, $description = NULL, $is_profile = FALSE, $profile_name = NULL) {
// Duplicate the default bundle to get its default configuration.
$default = $this->getBundle(FeaturesBundleInterface::DEFAULT_BUNDLE);
if (!$default) {
// If we don't have the default installed, generate it from the install
// config file.
$ext_storage = new ExtensionInstallStorage($this->configStorage);
$record = $ext_storage->read('features.bundle.default');
$bundle_storage = $this->entityManager->getStorage('features_bundle');
$default = $bundle_storage->createFromStorageRecord($record);
}
/** @var \Drupal\features\Entity\FeaturesBundle $bundle */
$bundle = $default->createDuplicate();
$bundle->setMachineName($machine_name);
$bundle->setName($name);
if (isset($description)) {
$bundle->setDescription($description);
}
else {
$bundle->setDescription(t('Auto-generated bundle from package @name', array('@name' => $name)));
}
$bundle->setIsProfile($is_profile);
if (isset($profile_name)) {
$bundle->setProfileName($profile_name);
}
$bundle->save();
$this->setBundle($bundle);
return $bundle;
}
/**
* {@inheritdoc}
*/
public function createBundlesFromPackages() {
$existing_bundles = $this->getBundleList();
$new_bundles = [];
// Only parse from installed features.
$modules = $this->featuresManager->getFeaturesModules(NULL, TRUE);
foreach ($modules as $module) {
$info = $this->featuresManager->getExtensionInfo($module);
// @todo This entire function could be simplified a lot using packages.
$features_info = $this->featuresManager->getFeaturesInfo($module);
// Create a new bundle if:
// - the feature specifies a bundle and
// - that bundle doesn't yet exist locally.
// Allow profiles to override previous values.
if (!empty($features_info['bundle']) &&
!isset($existing_bundles[$features_info['bundle']]) &&
(!in_array($features_info['bundle'], $new_bundles) || $info['type'] == 'profile')) {
if ($info['type'] == 'profile') {
$new_bundle = [
'name' => $info['name'],
'description' => $info['description'],
'is_profile' => TRUE,
'profile_name' => $module->getName(),
];
}
else {
$new_bundle = [
'name' => isset($info['package']) ? $info['package'] : ucwords(str_replace('_', ' ', $features_info['bundle'])),
'description' => NULL,
'is_profile' => FALSE,
'profile_name' => NULL,
];
}
$new_bundle['machine_name'] = $features_info['bundle'];
$new_bundles[$new_bundle['machine_name']] = $new_bundle;
}
}
foreach ($new_bundles as $new_bundle) {
$new_bundle = $this->createBundleFromDefault($new_bundle['machine_name'], $new_bundle['name'], $new_bundle['description'], $new_bundle['is_profile']);
drupal_set_message($this->t('Features bundle @name automatically created.', ['@name' => $new_bundle->getName()]));
}
}
/**
* {@inheritdoc}
*/
public function getBundleOptions() {
$list = $this->getBundleList();
$result = array();
foreach ($list as $machine_name => $bundle) {
$result[$machine_name] = $bundle->getName();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function applyBundle($machine_name = NULL) {
$this->reset();
$bundle = $this->loadBundle($machine_name);
if (isset($bundle)) {
$this->assignConfigPackages();
return $this->currentBundle;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function renameBundle($old_machine, $new_machine) {
$is_current = (isset($this->currentBundle) && ($old_machine == $this->currentBundle->getMachineName()));
$bundle = $this->getBundle($old_machine);
if ($bundle->getMachineName() != '') {
// Remove old bundle from the list if it's not the Default bundle.
unset($this->bundles[$old_machine]);
}
$bundle->setMachineName($new_machine);
$this->setBundle($bundle);
// Put the bundle into the list with the correct name.
$this->bundles[$bundle->getMachineName()] = $bundle;
if ($is_current) {
$this->setCurrent($bundle);
}
return $bundle;
}
/**
* {@inheritdoc}
*/
public function loadBundle($machine_name = NULL) {
if (!isset($machine_name)) {
$session = \Drupal::request()->getSession();
if (isset($session)) {
$machine_name = isset($session) ? $session->get('features_current_bundle', FeaturesBundleInterface::DEFAULT_BUNDLE) : FeaturesBundleInterface::DEFAULT_BUNDLE;
}
}
$bundle = $this->getBundle($machine_name);
if (!isset($bundle)) {
// If bundle no longer exists then return default.
$bundle = $this->bundles[FeaturesBundleInterface::DEFAULT_BUNDLE];
}
return $this->setCurrent($bundle);
}
/**
* {@inheritdoc}
*/
public function removeBundle($machine_name) {
$bundle = $this->getBundle($machine_name);
if (isset($bundle) && !$bundle->isDefault()) {
unset($this->bundles[$machine_name]);
$bundle->remove();
}
}
}
@@ -0,0 +1,228 @@
<?php
namespace Drupal\features;
/**
* Common interface for features assignment services.
*
* The feature API is based on two major concepts:
* - Packages: modules into which configuration is packaged.
* - Package assignment methods: responsible for `determining
* which package to assign a given piece of configuration to.
* Assignment methods are customizable.
*
* Features defines several package assignment methods, which are simple plugin
* classes that implement a particular logic to assign pieces of configuration
* to a given package (module).
*
* Modules can define additional package assignment methods by simply providing
* the related plugins, and alter existing methods through
* hook_features_assignment_method_info_alter(). Here is an example
* snippet:
* @code
* function mymodule_features_assignment_method_info_alter(&$assignment_info) {
* // Replace the original plugin with our own implementation.
* $method_id = \Drupal\features\Plugin\FeaturesAssignment\FeaturesAssignmentBaseType::METHOD_ID;
* $assignment_info[$method_id]['class'] = 'Drupal\my_module\Plugin\FeaturesAssignment\MyFeaturesAssignmentBaseType';
* }
*
* class MyFeaturesAssignmentBaseType extends FeaturesAssignmentBaseType {
* public function assignPackages($force = FALSE) {
* // Insert customization here.
* }
* }
* ?>
* @endcode
*
* For more information, see
* @link http://drupal.org/node/2404473 Developing for Features 3.x @endlink
*/
interface FeaturesAssignerInterface {
/**
* The package assignment method id for the package assigner itself.
*/
const METHOD_ID = 'assigner-default';
/**
* Resets the assigned packages and the method instances.
*/
public function reset();
/**
* Apply all enabled package assignment methods.
*
* @param bool $force
* (optional) If TRUE, assign config regardless of restrictions such as it
* being already assigned to a package.
*/
public function assignConfigPackages($force = FALSE);
/**
* Applies a given package assignment method.
*
* @param string $method_id
* The string identifier of the package assignment method to use to package
* configuration.
* @param bool $force
* (optional) If TRUE, assign config regardless of restrictions such as it
* being already assigned to a package.
*/
public function applyAssignmentMethod($method_id, $force = FALSE);
/**
* Returns the enabled package assignment methods.
*
* @return array
* An array of package assignment method IDs.
*/
public function getAssignmentMethods();
/**
* Resaves the configuration to purge missing assignment methods.
*/
public function purgeConfiguration();
/**
* Returns a FeaturesBundle object.
*
* @param string $name
* machine name of package set. If omitted, returns the current bundle.
*
* @return \Drupal\features\FeaturesBundleInterface
* A features bundle object.
*/
public function getBundle($name = NULL);
/**
* Stores a features bundle.
*
* Added to list if machine_name is new.
*
* @param \Drupal\features\FeaturesBundleInterface $bundle
* A features bundle.
* @param bool $current
* Determine if the current bundle is set to $bundle.
* If False, the current bundle is only updated if it already has the same
* machine name as the $bundle.
*/
public function setBundle(FeaturesBundleInterface $bundle, $current = TRUE);
/**
* Searches for a bundle that matches the $info.yml or $features.yml export.
*
* Creates a new bundle as needed.
*
* @param array $info
* The bundle info.
* @param mixed $features_info
* The features info.
*
* @return \Drupal\features\FeaturesBundleInterface
* A bundle.
*/
public function findBundle(array $info, $features_info = NULL);
/**
* Sets the currently active bundle.
*
* Updates value in current SESSION.
*
* @param \Drupal\features\FeaturesBundleInterface $bundle
* A features bundle object.
*/
public function setCurrent(FeaturesBundleInterface $bundle);
/**
* Returns an array of all existing features bundles.
*
* @return \Drupal\features\FeaturesBundleInterface[]
* Keyed by machine_name with value of
* \Drupal\features\FeaturesBundleInterface.
*/
public function getBundleList();
/**
* Returns a named bundle.
*
* First searches by Human name, then by machine_name.
*
* @param string $name
* The bundle name to search by.
*
* @return \Drupal\features\FeaturesBundleInterface
* A features bundle object.
*/
public function findBundleByName($name);
/**
* Creates a new bundle by duplicating the default bundle and customizing.
*
* @param string $machine_name
* Machine name.
* @param string $name
* (optional) Human readable name of the bundle.
* @param string $description
* (optional) Description of the bundle.
* @param bool $is_profile
* (optional) TRUE if a profile is used with this bundle.
* @param string $profile_name
* (optional) The machine name of the profile.
*
* @return \Drupal\features\FeaturesBundleInterface
* A features bundle object.
*/
public function createBundleFromDefault($name, $machine_name = '', $description = '', $is_profile = FALSE, $profile_name = NULL);
/**
* Creates bundles by parsing information from installed packages.
*/
public function createBundlesFromPackages();
/**
* Returns an array of bundle names suitable for a select option list.
*
* @return array
* An array of bundles, keyed by machine_name, with values being human
* readable names.
*/
public function getBundleOptions();
/**
* Makes the named bundle the current bundle.
*
* @param string $machine_name
* The name of a features bundle. If omitted, gets the last bundle from the
* Session.
*
* @return \Drupal\features\FeaturesBundleInterface
* A features bundle object.
*/
public function applyBundle($machine_name = NULL);
/**
* Renames a bundle.
*
* @param string $old_machine
* The old machine name of a bundle.
* @param string $new_machine
* The new machine name of a bundle.
*
* @return \Drupal\features\FeaturesBundleInterface
* A features bundle object.
*/
public function renameBundle($old_machine, $new_machine);
/**
* Loads a named bundle.
*
* @param string $machine_name
* (optional) The name of a features bundle.
* Defaults to NULL, gets the last bundle from the session.
*
* @return \Drupal\features\FeaturesBundleInterface
* A features bundle object.
*/
public function loadBundle($machine_name = NULL);
}
@@ -0,0 +1,124 @@
<?php
namespace Drupal\features;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\PluginBase;
/**
* Base class for package assignment methods.
*/
abstract class FeaturesAssignmentMethodBase extends PluginBase implements FeaturesAssignmentMethodInterface {
/**
* The features manager.
*
* @var \Drupal\features\FeaturesManagerInterface
*/
protected $featuresManager;
/**
* The features assigner.
*
* @var \Drupal\features\FeaturesAssignerInterface
*/
protected $assigner;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
public function setfeaturesManager(FeaturesManagerInterface $features_manager) {
$this->featuresManager = $features_manager;
}
/**
* {@inheritdoc}
*/
public function setAssigner(FeaturesAssignerInterface $assigner) {
$this->assigner = $assigner;
}
/**
* {@inheritdoc}
*/
public function setEntityManager(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public function setConfigFactory(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* Assigns configuration of the types specified in a setting to a package.
*
* @param string $machine_name
* Machine name of the package.
* @param bool $force
* (optional) If TRUE, assign config regardless of restrictions such as it
* being already assigned to a package.
*/
protected function assignPackageByConfigTypes($machine_name, $force = FALSE) {
$current_bundle = $this->assigner->getBundle();
$settings = $current_bundle->getAssignmentSettings($this->getPluginId());
$types = $settings['types']['config'];
$config_collection = $this->featuresManager->getConfigCollection();
foreach ($config_collection as $item_name => $item) {
// Don't assign configuration that's provided by an extension.
if (in_array($item->getType(), $types) && !($item->isProviderExcluded())) {
try {
$this->featuresManager->assignConfigPackage($machine_name, [$item_name]);
}
catch (\Exception $exception) {
\Drupal::logger('features')->error($exception->getMessage());
}
}
}
}
/**
* Assigns a given subdirectory to configuration of specified types.
*
* @param string $subdirectory
* The subdirectory that designated configuration should be exported to.
*/
protected function assignSubdirectoryByConfigTypes($subdirectory) {
$current_bundle = $this->assigner->getBundle();
$settings = $current_bundle->getAssignmentSettings($this->getPluginId());
$types = $settings['types']['config'];
if (!empty($types)) {
$config_collection = $this->featuresManager->getConfigCollection();
foreach ($config_collection as &$item) {
if (in_array($item->getType(), $types)) {
$item->setSubdirectory($subdirectory);
}
}
// Clean up the $item pass by reference.
unset($item);
$this->featuresManager->setConfigCollection($config_collection);
}
}
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\features;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
/**
* Interface for package assignment classes.
*/
interface FeaturesAssignmentMethodInterface extends PluginInspectionInterface {
/**
* Injects the features manager.
*
* @param \Drupal\features\FeaturesManagerInterface $features_manager
* The features manager to be used to retrieve the configuration list and
* the already assigned packages.
*/
public function setFeaturesManager(FeaturesManagerInterface $features_manager);
/**
* Injects the features assigner.
*
* @param \Drupal\features\FeaturesAssignerInterface $assigner
* The features assigner to be used to retrieve the bundle configuration.
*/
public function setAssigner(FeaturesAssignerInterface $assigner);
/**
* Injects the entity manager.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager to be used to retrieve entity information.
*/
public function setEntityManager(EntityManagerInterface $entity_manager);
/**
* Injects the configuration factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory to be used to retrieve configuration values.
*/
public function setConfigFactory(ConfigFactoryInterface $config_factory);
/**
* Performs package assignment.
*
* @param bool $force
* (optional) If TRUE, assign config regardless of restrictions such as it
* being already assigned to a package.
*/
public function assignPackages($force = FALSE);
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\features;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages configuration packaging methods.
*/
class FeaturesAssignmentMethodManager extends DefaultPluginManager {
/**
* Constructs a new FeaturesAssignmentMethodManager object.
*
* @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
* An object that implements CacheBackendInterface.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* An object that implements ModuleHandlerInterface.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/FeaturesAssignment', $namespaces, $module_handler,
'Drupal\features\FeaturesAssignmentMethodInterface');
$this->alterInfo('features_assignment_info');
$this->setCacheBackend($cache_backend, 'features_assignment_methods');
}
}
@@ -0,0 +1,240 @@
<?php
namespace Drupal\features;
/**
* Provides an interface for the FeaturesBundle object.
*/
interface FeaturesBundleInterface {
const DEFAULT_BUNDLE = 'default';
/**
* Determines whether the current bundle is the default one.
*
* @return bool
* Returns TRUE if this is the default bundle.
*/
public function isDefault();
/**
* Returns the machine name of a bundle.
*
* @return string
* The machine name of a bundle.
*
* @see \Drupal\features\FeaturesBundleInterface::setMachineName()
*/
public function getMachineName();
/**
* Sets the machine name of a bundle.
*
* @param string $machine_name
* The machine name of a bundle.
*
* @see \Drupal\features\FeaturesBundleInterface::getMachineName()
*/
public function setMachineName($machine_name);
/**
* Gets the human readable name of a bundle.
*
* @return string
* The human readable name of a bundle.
*
* @see \Drupal\features\FeaturesBundleInterface::setName()
*/
public function getName();
/**
* Sets the human readable name of a bundle.
*
* @param string $name
* The human readable name of a bundle.
*
* @see \Drupal\features\FeaturesBundleInterface::getName()
*/
public function setName($name);
/**
* Returns a full machine name prefixed with the bundle name.
*
* @param string $short_name
* The short machine_name of a bundle.
*
* @return string
* The full machine_name of a bundle.
*/
public function getFullName($short_name);
/**
* Returns a short machine name not prefixed with the bundle name.
*
* @param string $machine_name
* The full machine_name of a bundle.
*
* @return string
* The short machine_name of a bundle.
*/
public function getShortName($machine_name);
/**
* Determines if the $machine_name is prefixed by the bundle machine name.
*
* @param string $machine_name
* The machine name of a package.
*
* @return bool
* TRUE if the machine name is prefixed by the bundle machine name.
*/
public function inBundle($machine_name);
/**
* Determines if the package with $machine_name is the bundle profile.
*
* @param string $machine_name
* The machine name of a package.
*
* @return bool
* TRUE if the package with $machine_name is the bundle profile.
*/
public function isProfilePackage($machine_name);
/**
* Gets the description of a bundle.
*
* @return string
* The description of a bundle.
*
* @see \Drupal\features\FeaturesBundleInterface::setDescription()
*/
public function getDescription();
/**
* Sets the description of a bundle.
*
* @param string $description
* The description of a bundle.
*
* @see \Drupal\features\FeaturesBundleInterface::getDescription()
*/
public function setDescription($description);
/**
* Gets option for using a profile with this bundle.
*
* @return bool
* TRUE if a profile is used with this profile.
*/
public function isProfile();
/**
* Sets option for using a profile with this bundle.
*
* @param bool $value
* TRUE if a profile is used with this bundle.
*/
public function setIsProfile($value);
/**
* Returns the machine name of the profile.
*
* If the bundle doesn't use a profile, return the current site profile.
*
* @return string
* THe machie name of a profile.
*
* @see \Drupal\features\FeaturesBundleInterface::setProfileName()
*/
public function getProfileName();
/**
* Sets the name of the profile associated with this bundle.
*
* @param string $machine_name
* The machine name of a profile.
*
* @see \Drupal\features\FeaturesBundleInterface::getProfileName()
*/
public function setProfileName($machine_name);
/**
* Gets the list of enabled assignment methods.
*
* @return array
* An array of method IDs keyed by assignment method IDs.
*
* @see \Drupal\features\FeaturesBundleInterface::setEnabledAssignments()
*/
public function getEnabledAssignments();
/**
* Sets the list of enabled assignment methods.
*
* @param array $assignments
* An array of values keyed by assignment method IDs. Non-empty value is
* enabled.
*
* @see \Drupal\features\FeaturesBundleInterface::getEnabledAssignments()
*/
public function setEnabledAssignments(array $assignments);
/**
* Gets the weights of the assignment methods.
*
* @return array
* An array keyed by assignment method_id with a numeric weight.
*
* @see \Drupal\features\FeaturesBundleInterface::setAssignmentWeights()
*/
public function getAssignmentWeights();
/**
* Sets the weights of the assignment methods.
*
* @param array $assignments
* An array keyed by assignment method_id with a numeric weight value.
*
* @see \Drupal\features\FeaturesBundleInterface::getAssignmentWeights()
*/
public function setAssignmentWeights(array $assignments);
/**
* Gets settings specific to an assignment method.
*
* @param string $method_id
* The ID of an assignment method. If NULL, return all assignment settings
* keyed by method_id.
*
* @return array
* An array of settings. Format specific to assignment method.
*
* @see \Drupal\features\FeaturesBundleInterface::setAssignmentSettings()
*/
public function getAssignmentSettings($method_id = NULL);
/**
* Sets settings specific to an assignment method.
*
* @param string $method_id
* The ID of an assignment method. If NULL, all $settings are given keyed
* by method_ID.
* @param array $settings
* An array of setting values.
*
* @see \Drupal\features\FeaturesBundleInterface::getAssignmentSettings()
*/
public function setAssignmentSettings($method_id, array $settings);
/**
* Saves the bundle to the active config.
*/
public function save();
/**
* Removes the bundle from the active config.
*/
public function remove();
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\features;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Config\Entity\ConfigEntityDependency;
/**
* Class FeaturesConfigDependencyManager
* @package Drupal\features
*/
class FeaturesConfigDependencyManager extends ConfigDependencyManager{
protected $sorted_graph;
/**
* {@inheritdoc}
*/
public function getDependentEntities($type, $name) {
$dependent_entities = array();
$entities_to_check = array();
if ($type == 'config') {
$entities_to_check[] = $name;
}
else {
if ($type == 'module' || $type == 'theme' || $type == 'content') {
$dependent_entities = array_filter($this->data, function (ConfigEntityDependency $entity) use ($type, $name) {
return $entity->hasDependency($type, $name);
});
}
// If checking content, module, or theme dependencies, discover which
// entities are dependent on the entities that have a direct dependency.
foreach ($dependent_entities as $entity) {
$entities_to_check[] = $entity->getConfigDependencyName();
}
}
$dependencies = array_merge($this->createGraphConfigEntityDependencies($entities_to_check), $dependent_entities);
if (!$this->sorted_graph) {
// Sort dependencies in the reverse order of the graph. So the least
// dependent is at the top. For example, this ensures that fields are
// always after field storages. This is because field storages need to be
// created before a field.
$this->sorted_graph = $this->getGraph();
uasort($this->sorted_graph, array($this, 'sortGraph'));
}
return array_replace(array_intersect_key($this->sorted_graph, $dependencies), $dependencies);
}
/**
* {@inheritdoc}
*/
public function setData(array $data) {
parent::setData($data);
$this->sorted_graph = NULL;
return $this;
}
}
@@ -0,0 +1,51 @@
<?php
namespace Drupal\features;
use Drupal\Core\Config\ConfigInstaller;
use Drupal\Core\Config\StorageInterface;
/**
* Class for customizing the test for pre existing configuration.
*
* Copy of ConfigInstaller with findPreExistingConfiguration() modified to
* allow Feature modules to be installed.
*/
class FeaturesConfigInstaller extends ConfigInstaller {
/**
* {@inheritdoc}
*/
protected function findPreExistingConfiguration(StorageInterface $storage) {
// CHANGE START
// Override
// Drupal\Core\Config\ConfigInstaller::findPreExistingConfiguration().
// Allow config that already exists coming from Features.
/** @var \Drupal\features\FeaturesManagerInterface $manager */
$manager = \Drupal::service('features.manager');
$features_config = array_keys($manager->listExistingConfig());
// Map array so we can use isset instead of in_array for faster access.
$features_config = array_combine($features_config, $features_config);
// CHANGE END.
$existing_configuration = array();
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
foreach ($collection_info->getCollectionNames() as $collection) {
$config_to_create = array_keys($this->getConfigToCreate($storage, $collection));
$active_storage = $this->getActiveStorages($collection);
foreach ($config_to_create as $config_name) {
if ($active_storage->exists($config_name)) {
// CHANGE START
// Test if config is part of a Feature package.
if (!isset($features_config[$config_name])) {
// CHANGE END.
$existing_configuration[$collection][] = $config_name;
}
}
}
}
return $existing_configuration;
}
}
@@ -0,0 +1,121 @@
<?php
namespace Drupal\features;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Extension\Extension;
/**
* Wraps FeaturesInstallStorage to support multiple configuration
* directories.
*/
class FeaturesExtensionStorages implements FeaturesExtensionStoragesInterface {
/**
* The target storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* The extension storages.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $extensionStorages;
/**
* Configuration provided by extension storages.
*
* @var array
*/
protected $configurationLists;
/**
* Constructs a new FeaturesExtensionStorages object.
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The configuration storage.
*/
public function __construct(StorageInterface $config_storage) {
$this->configStorage = $config_storage;
}
/**
* {@inheritdoc}
*/
public function getExtensionStorages() {
return $this->extensionStorages;
}
/**
* {@inheritdoc}
*/
public function addStorage($directory = InstallStorage::CONFIG_INSTALL_DIRECTORY) {
$this->extensionStorages[$directory] = new FeaturesInstallStorage($this->configStorage, $directory);
$this->reset();
}
/**
* {@inheritdoc}
*/
public function read($name) {
$list = $this->listAllByDirectory('');
if (isset($list[$name])) {
$directory = $list[$name];
return $this->extensionStorages[$directory]->read($name);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
return array_keys($this->listAllByDirectory($prefix));
}
/**
* {@inheritdoc}
*/
public function listExtensionConfig(Extension $extension) {
$extension_config = [];
foreach ($this->extensionStorages as $directory => $extension_storage) {
$extension_config = array_merge($extension_config, array_keys($extension_storage->getComponentNames([
$extension->getName() => $extension,
])));
}
return $extension_config;
}
/**
* Resets packages and configuration assignment.
*/
protected function reset() {
$this->configurationLists = [];
}
/**
* Returns a list of all configuration available from extensions.
*
* @param string $prefix
* (optional) The prefix to search for. If omitted, all configuration object
* names that exist are returned.
*
* @return array
* An array with configuration item names as keys and configuration
* directories as values.
*/
protected function listAllByDirectory($prefix = '') {
if (!isset($this->configurationLists[$prefix])) {
$this->configurationLists[$prefix] = [];
foreach ($this->extensionStorages as $directory => $extension_storage) {
$this->configurationLists[$prefix] += array_fill_keys($extension_storage->listAll($prefix), $directory);
}
}
return $this->configurationLists[$prefix];
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\features;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Extension\Extension;
/**
* The FeaturesExtensionStorages provides a collection of extension storages,
* one for each supported configuration directory.
*
* Typically this will include the install and optional directories defined by
* Drupal core, but may also include any extension configuration directories
* added by contributed modules.
*
* This class serves as a partial wrapper to
* Drupal\Core\Config\StorageInterface, providing a subset of methods that can
* be called to apply to all available extension storages. For example,
* FeaturesExtensionStoragesInterface::read() will read an extension-provided
* configuration item regardless of which extension storage directory it is
* provided in.
*/
interface FeaturesExtensionStoragesInterface {
/**
* Returns all registered extension storages.
*
* @return FeaturesInstallStorage[]
* Array of install storages keyed by configuration directory.
*/
public function getExtensionStorages();
/**
* Adds a storage.
*
* @param string $directory
* (optional) The configuration directory. If omitted,
* InstallStorage::CONFIG_INSTALL_DIRECTORY will be used.
*/
public function addStorage($directory = InstallStorage::CONFIG_INSTALL_DIRECTORY);
/**
* Reads configuration data from the storages.
*
* @param string $name
* The name of a configuration object to load.
*
* @return array|bool
* The configuration data stored for the configuration object name. If no
* configuration data exists for the given name, FALSE is returned.
*/
public function read($name);
/**
* Gets configuration object names starting with a given prefix.
*
* Given the following configuration objects:
* - node.type.article
* - node.type.page
*
* Passing the prefix 'node.type.' will return an array containing the above
* names.
*
* @param string $prefix
* (optional) The prefix to search for. If omitted, all configuration object
* names that exist are returned.
*
* @return array
* An array containing matching configuration object names.
*/
public function listAll($prefix = '');
/**
* Lists names of configuration objects provided by a given extension.
*
* If a $name and/or $namespace is specified, only matching modules will be
* returned. Otherwise, all install are returned.
*
* @param mixed $extension
* A string name of an extension or a full Extension object.
*
* @return array
* An array of configuration object names.
*/
public function listExtensionConfig(Extension $extension);
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\features;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Base class for package assignment methods.
*/
abstract class FeaturesGenerationMethodBase implements FeaturesGenerationMethodInterface {
use StringTranslationTrait;
/**
* The features manager.
*
* @var \Drupal\features\FeaturesManagerInterface
*/
protected $featuresManager;
/**
* The features assigner.
*
* @var \Drupal\features\FeaturesAssignerInterface
*/
protected $assigner;
/**
* {@inheritdoc}
*/
public function setFeaturesManager(FeaturesManagerInterface $features_manager) {
$this->featuresManager = $features_manager;
}
/**
* {@inheritdoc}
*/
public function setAssigner(FeaturesAssignerInterface $assigner) {
$this->assigner = $assigner;
}
/**
* {@inheritdoc}
*/
public function exportFormSubmit(array &$form, FormStateInterface $form_state) {
}
/**
* Merges an info file into a package's info file.
*
* @param string $package_info
* The Yaml encoded package info.
* @param string $info_file_uri
* The info file's URI.
*/
protected function mergeInfoFile($package_info, $info_file_uri) {
$package_info = Yaml::decode($package_info);
/** @var \Drupal\Core\Extension\InfoParserInterface $existing_info */
$existing_info = \Drupal::service('info_parser')->parse($info_file_uri);
return Yaml::encode($this->featuresManager->mergeInfoArray($existing_info, $package_info));
}
/**
* {@inheritdoc}
*/
public function prepare(array &$packages = array(), FeaturesBundleInterface $bundle = NULL) {
// If no packages were specified, get all packages.
if (empty($packages)) {
$packages = $this->featuresManager->getPackages();
}
// If any packages exist, read in their files.
$existing_packages = $this->featuresManager->listPackageDirectories(array_keys($packages), $bundle);
foreach ($packages as &$package) {
list($full_name, $path) = $this->featuresManager->getExportInfo($package, $bundle);
if (empty($package->getDirectory())) {
$package->setDirectory($path);
}
// If this is the profile, its directory is already assigned.
if (!isset($bundle) || !$bundle->isProfilePackage($package->getMachineName())) {
$package->setDirectory($package->getDirectory() . '/' . $full_name);
}
$this->preparePackage($package, $existing_packages, $bundle);
}
// Clean up the $package pass by reference.
unset($package);
}
/**
* Performs any required changes on a package prior to generation.
*
* @param \Drupal\features\Package $package
* The package to be prepared.
* @param array $existing_packages
* An array of existing packages with machine names as keys and paths as
* values.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* Optional bundle used for export
*/
abstract protected function preparePackage(Package $package, array $existing_packages, FeaturesBundleInterface $bundle = NULL);
}
@@ -0,0 +1,67 @@
<?php
namespace Drupal\features;
use Drupal\Core\Form\FormStateInterface;
/**
* Interface for package assignment classes.
*/
interface FeaturesGenerationMethodInterface {
/**
* Injects the features manager.
*
* @param \Drupal\features\FeaturesManagerInterface $features_manager
* The features manager to be used to retrieve the configuration
* list and the assigned packages.
*/
public function setFeaturesManager(FeaturesManagerInterface $features_manager);
/**
* Injects the features assigner.
*
* @param \Drupal\features\FeaturesAssignerInterface $assigner
* The features assigner to be used to retrieve the bundle configuration.
*/
public function setAssigner(FeaturesAssignerInterface $assigner);
/**
* Prepares packages for generation.
*
* @param array $packages
* Array of package data.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The optional bundle used for the generation. Used to generate profiles.
*
* @return array
* An array of packages data.
*/
public function prepare(array &$packages = array(), FeaturesBundleInterface $bundle = NULL);
/**
* Performs package generation.
*
* @param array $packages
* Array of package data.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The optional bundle used for the generation. Used to generate profiles.
*
* @return array
* Array of results for profile and/or packages, each result including the
* following keys:
* - 'success': boolean TRUE or FALSE for successful writing.
* - 'display': boolean TRUE if the message should be displayed to the
* user, otherwise FALSE.
* - 'message': a message about the result of the operation.
* - 'variables': an array of substitutions to be used in the message.
*/
public function generate(array $packages = array(), FeaturesBundleInterface $bundle = NULL);
/**
* Responds to the submission of
* \Drupal\features_ui\Form\FeaturesExportForm.
*/
public function exportFormSubmit(array &$form, FormStateInterface $form_state);
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\features;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages configuration packaging methods.
*/
class FeaturesGenerationMethodManager extends DefaultPluginManager {
/**
* Constructs a new FeaturesGenerationMethodManager object.
*
* @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
* An object that implements CacheBackendInterface.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* An object that implements ModuleHandlerInterface.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/FeaturesGeneration', $namespaces, $module_handler, 'Drupal\features\FeaturesGenerationMethodInterface');
$this->cacheBackend = $cache_backend;
$this->cacheKeyPrefix = 'features_generation_methods';
$this->cacheKey = 'features_generation_methods';
$this->alterInfo('features_generation_info');
}
}
@@ -0,0 +1,173 @@
<?php
namespace Drupal\features;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Class responsible for performing package generation.
*/
class FeaturesGenerator implements FeaturesGeneratorInterface {
use StringTranslationTrait;
/**
* The package generation method plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $generatorManager;
/**
* The features manager.
*
* @var \Drupal\features\FeaturesManagerInterface
*/
protected $featuresManager;
/**
* The features assigner.
*
* @var \Drupal\features\FeaturesAssignerInterface
*/
protected $assigner;
/**
* Local cache for package generation method instances.
*
* @var array
*/
protected $methods;
/**
* Constructs a new FeaturesGenerator object.
*
* @param \Drupal\features\FeaturesManagerInterface $features_manager
* The features manager.
* @param \Drupal\Component\Plugin\PluginManagerInterface $generator_manager
* The package generation methods plugin manager.
*/
public function __construct(FeaturesManagerInterface $features_manager, PluginManagerInterface $generator_manager, FeaturesAssignerInterface $assigner) {
$this->featuresManager = $features_manager;
$this->generatorManager = $generator_manager;
$this->assigner = $assigner;
}
/**
* Initializes the injected features manager with the generator.
*
* This should be called right after instantiating the generator to make it
* available to the features manager without introducing a circular
* dependency.
*/
public function initFeaturesManager() {
$this->featuresManager->setGenerator($this);
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->methods = array();
}
/**
* {@inheritdoc}
*/
public function applyGenerationMethod($method_id, array $packages = array(), FeaturesBundleInterface $bundle = NULL) {
$method = $this->getGenerationMethodInstance($method_id);
$method->prepare($packages, $bundle);
return $method->generate($packages, $bundle);
}
/**
* {@inheritdoc}
*/
public function applyExportFormSubmit($method_id, &$form, FormStateInterface $form_state) {
$method = $this->getGenerationMethodInstance($method_id);
$method->exportFormSubmit($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function getGenerationMethods() {
return $this->generatorManager->getDefinitions();
}
/**
* Returns an instance of the specified package generation method.
*
* @param string $method_id
* The string identifier of the package generation method to use to package
* configuration.
*
* @return \Drupal\features\FeaturesGenerationMethodInterface
*/
protected function getGenerationMethodInstance($method_id) {
if (!isset($this->methods[$method_id])) {
$instance = $this->generatorManager->createInstance($method_id, array());
$instance->setFeaturesManager($this->featuresManager);
$instance->setAssigner($this->assigner);
$this->methods[$method_id] = $instance;
}
return $this->methods[$method_id];
}
/**
* {@inheritdoc}
*/
public function generatePackages($method_id, FeaturesBundleInterface $bundle, array $package_names = array()) {
$this->featuresManager->setPackageBundleNames($bundle, $package_names);
return $this->generate($method_id, $bundle, $package_names);
}
/**
* Generates a file representation of configuration packages and, optionally,
* an install profile.
*
* @param string $method_id
* The ID of the generation method to use.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The bundle used for the generation.
* @param string[] $package_names
* Names of packages to be generated. If none are specified, all
* available packages will be added.
*
* @return array
* Array of results for profile and/or packages, each result including the
* following keys:
* - 'success': boolean TRUE or FALSE for successful writing.
* - 'display': boolean TRUE if the message should be displayed to the
* user, otherwise FALSE.
* - 'message': a message about the result of the operation.
* - 'variables': an array of substitutions to be used in the message.
*/
protected function generate($method_id, FeaturesBundleInterface $bundle, array $package_names = array()) {
$packages = $this->featuresManager->getPackages();
// Filter out the packages that weren't requested.
if (!empty($package_names)) {
$packages = array_intersect_key($packages, array_fill_keys($package_names, NULL));
}
$this->featuresManager->assignInterPackageDependencies($bundle, $packages);
// Prepare the files.
$this->featuresManager->prepareFiles($packages);
$return = $this->applyGenerationMethod($method_id, $packages, $bundle);
foreach ($return as $message) {
if ($message['display']) {
$type = $message['success'] ? 'status' : 'error';
drupal_set_message($this->t($message['message'], $message['variables']), $type);
}
$type = $message['success'] ? 'notice' : 'error';
\Drupal::logger('features')->{$type}($message['message'], $message['variables']);
}
return $return;
}
}
@@ -0,0 +1,103 @@
<?php
namespace Drupal\features;
use Drupal\Core\Form\FormStateInterface;
/**
* Common interface for features generation services.
*
* The configuration packaging API is based on two major concepts:
* - Packages: modules into which configuration is packaged.
* - Package generation methods: responsible for `determining
* which package to assign a given piece of configuration to.
* Generation methods are customizable.
*
* Features defines two package generation methods, which are simple plugin
* classes that implement a particular logic to assign pieces of configuration
* to a given package (module).
*
* Modules can define additional package generation methods by simply providing
* the related plugins, and alter existing methods through
* hook_features_generation_method_info_alter(). Here is an example
* snippet:
* @code
* function mymodule_features_generation_method_info_alter(&$generation_info) {
* // Replace the original plugin with our own implementation.
* $method_id = \Drupal\features\Plugin\FeaturesGeneration\FeaturesGenerationArchive::METHOD_ID;
* $generation_info[$method_id]['class'] = 'Drupal\my_module\Plugin\FeaturesGeneration\MyFeaturesGenerationArchive';
* }
*
* class MyFeaturesGenerationArchive extends FeaturesGenerationArchive {
* public function generate(array $packages = array(), FeaturesBundleInterface $bundle = NULL) {
* // Insert customization here.
* }
* }
* ?>
* @endcode
*
* For more information, see
* @link http://drupal.org/node/2404473 Developing for Features 3.x @endlink
*/
interface FeaturesGeneratorInterface {
/**
* The package generation method id for the package generator itself.
*/
const METHOD_ID = 'generator-default';
/**
* Resets the assigned packages and the method instances.
*/
public function reset();
/**
* Apply a given package generation method.
*
* @param string $method_id
* The string identifier of the package generation method to use to package
* configuration.
* @param array $packages
* Array of package data.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The optional bundle used for the generation. Used to generate profiles.
*
* @return array
* Array of results for profile and/or packages, each result including the
* following keys:
* - 'success': boolean TRUE or FALSE for successful writing.
* - 'display': boolean TRUE if the message should be displayed to the
* user, otherwise FALSE.
* - 'message': a message about the result of the operation.
* - 'variables': an array of substitutions to be used in the message.
*/
public function applyGenerationMethod($method_id, array $packages = array(), FeaturesBundleInterface $bundle = NULL);
/**
* Responds to the submission of
* \Drupal\features_ui\Form\FeaturesExportForm.
*/
public function applyExportFormSubmit($method_id, &$form, FormStateInterface $form_state);
/**
* Returns the enabled package generation methods.
*
* @return array
* An array of package generation method definitions keyed by method id.
*/
public function getGenerationMethods();
/**
* Generates file representations of configuration packages.
*
* @param string $method_id
* The ID of the generation method to use.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The bundle used for the generation.
* @param array $package_names
* Array of names of packages to be generated. If none are specified, all
* available packages will be added.
*/
public function generatePackages($method_id, FeaturesBundleInterface $bundle, array $package_names = array());
}
@@ -0,0 +1,157 @@
<?php
namespace Drupal\features;
use Drupal\Core\Site\Settings;
use Drupal\Core\Config\ExtensionInstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Storage to access configuration and schema in installed extensions.
*
* Overrides the normal ExtensionInstallStorage to prevent profile from
* overriding.
*
* Also supports modules that are not installed yet.
*
* @see \Drupal\Core\Config\ExtensionInstallStorage
*/
class FeaturesInstallStorage extends ExtensionInstallStorage {
/**
* Overrides \Drupal\Core\Config\ExtensionInstallStorage::__construct().
*
* Sets includeProfile to FALSE.
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The active configuration store where the list of installed modules and
* themes is stored.
* @param string $directory
* The directory to scan in each extension to scan for files. Defaults to
* 'config/install'.
* @param string $collection
* (optional) The collection to store configuration in. Defaults to the
* default collection.
*/
public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) {
parent::__construct($config_storage, $directory, $collection, FALSE);
}
/**
* Returns a map of all config object names and their folders.
*
* The list is based on installed modules and themes. The active
* configuration storage is used rather than
* \Drupal\Core\Extension\ModuleHandler and
* \Drupal\Core\Extension\ThemeHandler in order to resolve circular
* dependencies between these services and
* \Drupal\Core\Config\ConfigInstaller and
* \Drupal\Core\Config\TypedConfigManager.
*
* NOTE: This code is copied from ExtensionInstallStorage::getAllFolders() with
* the following changes (Notes in CHANGED below)
* - Load all modules whether installed or not
*
* @return array
* An array mapping config object names with directories.
*/
public function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = array();
$this->folders += $this->getCoreNames();
$install_profile = Settings::get('install_profile');
$profile = drupal_get_profile();
$extensions = $this->configStorage->read('core.extension');
// @todo Remove this scan as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
// CHANGED START: Add profile directories for any bundles that use a profile.
$profile_directories = [];
if ($profile) {
$profile_directories[] = drupal_get_path('profile', $profile);
}
if ($this->includeProfile) {
// Add any profiles used in bundles.
/** @var \Drupal\features\FeaturesAssignerInterface $assigner */
$assigner = \Drupal::service('features_assigner');
$bundles = $assigner->getBundleList();
foreach ($bundles as $bundle_name => $bundle) {
if ($bundle->isProfile()) {
// Register the profile directory.
$profile_directory = 'profiles/' . $bundle->getProfileName();
if (is_dir($profile_directory)) {
$profile_directories[] = $profile_directory;
}
}
}
}
$listing->setProfileDirectories($profile_directories);
// CHANGED END
if (!empty($extensions['module'])) {
// CHANGED START: Find ANY modules, not just installed ones.
//$modules = $extensions['module'];
$module_list_scan = $listing->scan('module');
$modules = $module_list_scan;
// CHANGED END
// Remove the install profile as this is handled later.
unset($modules[$install_profile]);
$profile_list = $listing->scan('profile');
if ($profile && isset($profile_list[$profile])) {
// Prime the drupal_get_filename() static cache with the profile info
// file location so we can use drupal_get_path() on the active profile
// during the module scan.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname());
}
// CHANGED START: Put Features modules first in list returned.
// to allow features to override config provided by other extensions.
$featuresManager = \Drupal::service('features.manager');
$features_list = array();
$module_list = array();
foreach (array_keys($module_list_scan) as $module) {
if ($featuresManager->isFeatureModule($module_list_scan[$module])) {
$features_list[$module] = $module_list_scan[$module];
}
else {
$module_list[$module] = $module_list_scan[$module];
}
}
$this->folders += $this->getComponentNames($features_list);
$this->folders += $this->getComponentNames($module_list);
// CHANGED END
}
if (!empty($extensions['theme'])) {
$theme_list_scan = $listing->scan('theme');
foreach (array_keys($extensions['theme']) as $theme) {
if (isset($theme_list_scan[$theme])) {
$theme_list[$theme] = $theme_list_scan[$theme];
}
}
$this->folders += $this->getComponentNames($theme_list);
}
if ($this->includeProfile) {
// The install profile can override module default configuration. We do
// this by replacing the config file path from the module/theme with the
// install profile version if there are any duplicates.
if (isset($profile)) {
if (!isset($profile_list)) {
$profile_list = $listing->scan('profile');
}
if (isset($profile_list[$profile])) {
$profile_folders = $this->getComponentNames(array($profile_list[$profile]));
$this->folders = $profile_folders + $this->folders;
}
}
}
}
return $this->folders;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,601 @@
<?php
namespace Drupal\features;
use Drupal\Core\Extension\Extension;
/**
* Provides an interface for the FeaturesManager.
*/
interface FeaturesManagerInterface {
/**
* Simple configuration.
*
* Core uses system.simple, but since we're using this key in configuration
* arrays we can't include a period.
*
* @see https://www.drupal.org/node/2297311
*/
const SYSTEM_SIMPLE_CONFIG = 'system_simple';
/**
* Constants for package/module status.
*/
const STATUS_NO_EXPORT = 0;
const STATUS_UNINSTALLED = 1;
const STATUS_INSTALLED = 2;
const STATUS_DEFAULT = self::STATUS_NO_EXPORT;
/**
* Constants for package/module state.
*/
const STATE_DEFAULT = 0;
const STATE_OVERRIDDEN = 1;
/**
* Returns the active config store.
*
* @return \Drupal\Core\Config\StorageInterface
*/
public function getActiveStorage();
/**
* Returns a set of config storages.
*
* This method is used for support of multiple extension configuration
* directories, including the core-provided install and optional directories.
*
* @return \Drupal\Core\Config\StorageInterface[]
*/
public function getExtensionStorages();
/**
* Resets packages and configuration assignment.
*/
public function reset();
/**
* Gets an array of site configuration.
*
* @param bool $reset
* If TRUE, recalculate the configuration (undo all assignment methods).
*
* @return \Drupal\features\ConfigurationItem[]
* An array of items, each with the following keys:
* - 'name': prefixed configuration item name.
* - 'name_short': configuration item name without prefix.
* - 'label': human readable name of configuration item.
* - 'type': type of configuration.
* - 'data': the contents of the configuration item in exported format.
* - 'dependents': array of names of dependent configuration items.
* - 'subdirectory': feature subdirectory to export item to.
* - 'package': machine name of a package the configuration is assigned to.
* - 'extension_provided': whether the configuration is provided by an
* extension.
* - 'package_excluded': array of package names that this item should be
* excluded from.
*/
public function getConfigCollection($reset = FALSE);
/**
* Sets an array of site configuration.
*
* @param \Drupal\features\ConfigurationItem[] $config_collection
* An array of items.
*/
public function setConfigCollection(array $config_collection);
/**
* Gets an array of packages.
*
* @return \Drupal\features\Package[]
* An array of items, each with the following keys:
* - 'machine_name': machine name of the package such as 'example_article'.
* 'article'.
* - 'name': human readable name of the package such as 'Example Article'.
* - 'description': description of the package.
* - 'type': type of Drupal project ('module').
* - 'core': Drupal core compatibility ('8.x').
* - 'dependencies': array of module dependencies.
* - 'themes': array of names of themes to install.
* - 'config': array of names of configuration items.
* - 'status': status of the package. Valid values are:
* - FeaturesManagerInterface::STATUS_NO_EXPORT
* - FeaturesManagerInterface::STATUS_INSTALLED
* - FeaturesManagerInterface::STATUS_UNINSTALLED
* - 'version': version of the extension.
* - 'state': state of the extension. Valid values are:
* - FeaturesManagerInterface::STATE_DEFAULT
* - FeaturesManagerInterface::STATE_OVERRIDDEN
* - 'directory': the extension's directory.
* - 'files' array of files, each having the following keys:
* - 'filename': the name of the file.
* - 'subdirectory': any subdirectory of the file within the extension
* directory.
* - 'string': the contents of the file.
* - 'bundle': name of the features bundle this package belongs to.
* - 'extension': \Drupal\Core\Extension\Extension object.
* - 'info': the original info array from an existing package.
* - 'config_info': the original config of the module.
*
* @see \Drupal\features\FeaturesManagerInterface::setPackages()
*/
public function getPackages();
/**
* Sets an array of packages.
*
* @param \Drupal\features\Package[] $packages
* An array of packages.
*/
public function setPackages(array $packages);
/**
* Gets a specific package.
*
* @param string $machine_name
* Full machine name of package.
*
* @return \Drupal\features\Package
* Package data.
*
* @see \Drupal\features\FeaturesManagerInterface::getPackages()
*/
public function getPackage($machine_name);
/**
* Gets a specific package.
* Similar to getPackage but will also match package FullName
*
* @param string $machine_name
* Full machine name of package.
*
* @return \Drupal\features\Package
* Package data.
*
* @see \Drupal\features\FeaturesManagerInterface::getPackages()
*/
public function findPackage($machine_name);
/**
* Updates a package definition in the package list.
*
* NOTE: This does not "export" the package; it simply updates the internal
* data.
*
* @param \Drupal\features\Package $package
* The package.
*/
public function setPackage(Package $package);
/**
* Filters the supplied package list by the given namespace.
*
* @param \Drupal\features\Package[] $packages
* An array of packages.
* @param string $namespace
* The namespace to use.
* @param bool $only_exported
* If true, only filter out packages that are exported
*
* @return \Drupal\features\Package[]
* An array of packages.
*/
public function filterPackages(array $packages, $namespace = '', $only_exported = FALSE);
/**
* Gets a reference to a package assigner.
*
* @return \Drupal\features\FeaturesAssignerInterface
* The package assigner.
*/
public function getAssigner();
/**
* Injects the package assigner.
*
* @param \Drupal\features\FeaturesAssignerInterface $assigner
* The package assigner.
*/
public function setAssigner(FeaturesAssignerInterface $assigner);
/**
* Gets a reference to a package generator.
*
* @return \Drupal\features\FeaturesGeneratorInterface
* The package generator.
*/
public function getGenerator();
/**
* Injects the package generator.
*
* @param \Drupal\features\FeaturesGeneratorInterface $generator
* The package generator.
*/
public function setGenerator(FeaturesGeneratorInterface $generator);
/**
* Returns the current export settings.
*
* @return array
* An array with the following keys:
* - 'folder' - subdirectory to export packages to.
* - 'namespace' - module namespace being exported.
*/
public function getExportSettings();
/**
* Returns the current general features settings.
*
* @return \Drupal\Core\Config\Config
* A config object containing settings.
*/
public function getSettings();
/**
* Returns the contents of an extensions info.yml file.
*
* @param \Drupal\Core\Extension\Extension $extension
* An Extension object.
*
* @return array
* An array representing data in an info.yml file.
*/
public function getExtensionInfo(Extension $extension);
/**
* Determine if extension is enabled
*
* @param \Drupal\Core\Extension\Extension $extension
* @return bool
*/
public function extensionEnabled(Extension $extension);
/**
* Initializes a configuration package.
*
* @param string $machine_name
* Machine name of the package.
* @param string $name
* (optional) Human readable name of the package.
* @param string $description
* (optional) Description of the package.
* @param string $type
* (optional) Type of project.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* (optional) Bundle to use to add profile directories to the scan.
* @param \Drupal\Core\Extension\Extension $extension
* (optional) An Extension object.
* @return array
* The created package array.
*/
public function initPackage($machine_name, $name = NULL, $description = '', $type = 'module', FeaturesBundleInterface $bundle = NULL, Extension $extension = NULL);
/**
* Initializes a configuration package using module info data.
*
* @param \Drupal\Core\Extension\Extension $extension
* An Extension object.
*
* @return \Drupal\features\Package
* The created package array.
*/
public function initPackageFromExtension(Extension $extension);
/**
* Lists directories in which packages are present.
*
* This method scans to find package modules whether or not they are
* currently active (installed). As well as the directories that are
* usually scanned for modules and profiles, a profile directory for the
* current profile is scanned if it exists. For example, if the value
* for $bundle->getProfileName() is 'example', a
* directory profiles/example will be scanned if it exists. Therefore, when
* regenerating package modules, existing ones from a prior export will be
* recognized.
*
* @param string[] $machine_names
* Package machine names to return directories for. If omitted, return all
* directories.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* Optional bundle to use to add profile directories to the scan.
*
* @return array
* Array of package directories keyed by package machine name.
*/
public function listPackageDirectories(array $machine_names = array(), FeaturesBundleInterface $bundle = NULL);
/**
* Assigns a set of configuration items to a given package or profile.
*
* @param string $package_name
* Machine name of a package or the profile.
* @param string[] $item_names
* Configuration item names.
* @param bool $force
* (optional) If TRUE, assign config regardless of restrictions such as it
* being already assigned to a package.
*
* @throws Exception
*/
public function assignConfigPackage($package_name, array $item_names, $force = FALSE);
/**
* Assigns configuration items with names matching given strings to given
* packages.
*
* @param array $patterns
* Array with string patterns as keys and package machine names as values.
*/
public function assignConfigByPattern(array $patterns);
/**
* For given configuration items, assigns any dependent configuration to the
* same package.
*
* @param string[] $item_names
* Configuration item names.
* @param string $package
* Short machine name of package to assign dependent config to. If NULL,
* use the current package of the parent config items.
*/
public function assignConfigDependents(array $item_names = NULL, $package = NULL);
/**
* Adds the optional bundle prefix to package machine names.
*
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The bundle used for the generation.
* @param string[] &$package_names
* (optional) Array of package names, passed by reference.
*/
public function setPackageBundleNames(FeaturesBundleInterface $bundle, array &$package_names = []);
/**
* Assigns dependencies from config items into the package.
*
* @param \Drupal\features\Package[] $packages
* An array of packages. NULL for all packages
*/
public function assignPackageDependencies(Package $package = NULL);
/**
* Assigns dependencies between packages based on configuration dependencies.
*
* \Drupal\features\FeaturesBundleInterface::setPackageBundleNames() must be
* called prior to calling this method.
*
* @param \Drupal\features\FeaturesBundleInterface $bundle
* A features bundle.
* @param \Drupal\features\Package[] $packages
* An array of packages.
*/
public function assignInterPackageDependencies(FeaturesBundleInterface $bundle, array &$packages);
/**
* Merges two info arrays and processes the resulting array.
*
* Ensures values are unique and sorted.
*
* @param array $info1
* The first array.
* @param array $info2
* The second array.
* @param string[] $keys
* Keys to merge. If not specified, all keys present will be merged.
*
* @return array
* An array with the merged and processed results.
*
* @fixme Should this be moved to the package object or a related helper?
*/
public function mergeInfoArray(array $info1, array $info2, array $keys = array());
/**
* Lists the types of configuration available on the site.
*
* @param boolean $bundles_only
* Whether to list only configuration types that provide bundles.
*
* @return array
* An array with machine name keys and human readable values.
*/
public function listConfigTypes($bundles_only = FALSE);
/**
* Lists stored configuration for a given configuration type.
*
* @param string $config_type
* The type of configuration.
*/
public function listConfigByType($config_type);
/**
* Returns a list of all modules present on the site's file system.
*
* @return Drupal\Core\Extension\Extension[]
* An array of extension objects.
*/
public function getAllModules();
/**
* Returns a list of Features modules regardless of if they are installed.
*
* @param \Drupal\features\FeaturesBundleInterface $bundle
* Optional bundle to filter module list.
* If given, only modules matching the bundle namespace will be returned.
* If the bundle uses a profile, only modules in the profile will be
* returned.
* @param bool $installed
* List only installed modules.
*
* @return Drupal\Core\Extension\Extension[]
* An array of extension objects.
*/
public function getFeaturesModules(FeaturesBundleInterface $bundle = NULL, $installed = FALSE);
/**
* Lists names of configuration objects provided by a given extension.
*
* @param \Drupal\Core\Extension\Extension $extension
* An Extension object.
*
* @return array
* An array of configuration object names.
*/
public function listExtensionConfig(Extension $extension);
/**
* Lists names of configuration items provided by existing Features modules.
*
* @param bool $installed
* List only installed Features.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* (optional) Bundle to find existing configuration for.
*
* @return array
* An array with config names as keys and providing module names as values.
*/
public function listExistingConfig($installed = FALSE, FeaturesBundleInterface $bundle = NULL);
/**
* Iterates through packages and prepares file names and contents.
*
* @param array $packages
* An array of packages.
*/
public function prepareFiles(array $packages);
/**
* Returns the full name of a config item.
*
* @param string $type
* The config type, or '' to indicate $name is already prefixed.
* @param string $name
* The config name, without prefix.
*
* @return string
* The config item's full name.
*/
public function getFullName($type, $name);
/**
* Returns the short name and type of a full config name.
*
* @param string $fullname
* The full configuration name
* @return array
* 'type' => string the config type
* 'name_short' => string the short config name, without prefix.
*/
public function getConfigType($fullname);
/**
* Returns the full machine name and directory for exporting a package.
*
* @param \Drupal\features\Package $package
* The package.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* Optional bundle being used for export.
*
* @return array
* An array with the full name as the first item and directory as second
* item.
*/
public function getExportInfo(Package $package, FeaturesBundleInterface $bundle = NULL);
/**
* Determines if the module is a Features package, optinally testing by
* bundle.
*
* @param \Drupal\Core\Extension\Extension $module
* An extension object.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* (optional) Bundle to filter by.
*
* @return bool
* TRUE if the given module is a Features package of the given bundle (if any).
*/
public function isFeatureModule(Extension $module, FeaturesBundleInterface $bundle);
/**
* Determines which config is overridden in a package.
*
* @param \Drupal\features\Package $feature
* The package array.
* The 'state' property is updated if overrides are detected.
* @param bool $include_new
* If set, include newly detected config not yet exported.
*
* @result array $different
* The array of config items that are overridden.
*
* @see \Drupal\features\FeaturesManagerInterface::detectNew()
*/
public function detectOverrides(Package $feature, $include_new = FALSE);
/**
* Determines which config has not been exported to the feature.
*
* Typically added as an auto-detected dependency.
*
* @param \Drupal\features\Package $feature
* The package array.
*
* @return array
* The array of config items that are overridden.
*/
public function detectNew(Package $feature);
/**
* Determines which config is exported in the feature but not in the active.
*
* @param \Drupal\features\Package $feature
* The package array.
*
* @return array
* The array of config items that are missing from active store.
*/
public function detectMissing(Package $feature);
/**
* Sort the Missing config into order by dependencies.
* @param array $missing config items
* @return array of config items in dependency order
*/
public function reorderMissing(array $missing);
/**
* Helper function that returns a translatable label for the different status
* constants.
*
* @param int $status
* A status constant.
*
* @return string
* A translatable label.
*/
public function statusLabel($status);
/**
* Helper function that returns a translatable label for the different state
* constants.
*
* @param int $state
* A state constant.
*
* @return string
* A translatable label.
*/
public function stateLabel($state);
/**
* @param \Drupal\Core\Extension\Extension $extension
*
* @return array
*/
public function getFeaturesInfo(Extension $extension);
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\features;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Service provider implementation for Features to override config.installer.
*
* @ingroup container
*/
class FeaturesServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
// Override the config.installer class with a new class.
$definition = $container->getDefinition('config.installer');
$definition->setClass('Drupal\features\FeaturesConfigInstaller');
}
}
@@ -0,0 +1,549 @@
<?php
namespace Drupal\features;
/**
* Defines a value object for storing package related data.
*
* A package contains of a name, version number, containing config etc.
*/
class Package {
/**
* @var string
*/
protected $machineName = '';
/**
* @var string
*/
protected $name = '';
/**
* @var string
*/
protected $description = '';
/**
* @todo This could be fetched from the extension object.
*
* @var string
*/
protected $version = '';
/**
* @var string
*/
protected $core = '8.x';
/**
* @todo This could be fetched from the extension object.
*
* @var string
*/
protected $type = 'module';
/**
* @var string[]
*/
protected $themes = [];
/**
* @var string
*/
protected $bundle;
/**
* @var string[]
*/
protected $excluded = [];
/**
* @var string[]|bool
*/
protected $required = false;
/**
* @var array
*/
protected $info = [];
/**
* @var string[]
*/
protected $dependencies = [];
/**
* @todo This could be fetched from the extension object.
*
* @var int
*/
protected $status;
/**
* @var int
*/
protected $state;
/**
* @todo This could be fetched from the extension object.
*
* @var string
*/
protected $directory;
/**
* @var string[]
*/
protected $files;
/**
* @var \Drupal\Core\Extension\Extension
*/
protected $extension;
/**
* @var string[]
*/
protected $config = [];
/**
* @var string[]
*/
protected $configOrig = [];
/**
* Creates a new Package instance.
*
* @param string $machine_name
* The machine name.
* @param array $additional_properties
* (optional) Additional properties of the object.
*/
public function __construct($machine_name, array $additional_properties = []) {
$this->machineName = $machine_name;
$properties = get_object_vars($this);
foreach ($additional_properties as $property => $value) {
if (!array_key_exists($property, $properties)) {
throw new \InvalidArgumentException('Invalid property: ' . $property);
}
$this->{$property} = $value;
}
}
/**
* @return mixed
*/
public function getMachineName() {
return $this->machineName;
}
/**
* Return TRUE if the machine_name already has the bundle prefix.
*
* @param string $machine_name
* @param string $bundle_name
* @return bool
*/
protected function inBundle($machine_name, $bundle_name) {
return strpos($machine_name, $bundle_name . '_') === 0;
}
/**
* @return string
*/
public function getFullName() {
if (empty($this->bundle) || $this->inBundle($this->machineName, $this->bundle)) {
return $this->machineName;
}
else {
return $this->bundle . '_' . $this->machineName;
}
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @return string
*/
public function getDescription() {
return $this->description;
}
/**
* @return string
*/
public function getVersion() {
return $this->version;
}
/**
* @return int
*/
public function getStatus() {
return $this->status;
}
/**
* @return string[]
*/
public function getConfig() {
return $this->config;
}
/**
* Append a new filename.
*
* @param string $config
*
* @return $this
*/
public function appendConfig($config) {
$this->config[] = $config;
$this->config = array_unique($this->config);
return $this;
}
public function removeConfig($name) {
$this->config = array_diff($this->config, [$name]);
return $this;
}
/**
* @return string
*/
public function getBundle() {
return $this->bundle;
}
/**
* @return string[]
*/
public function getExcluded() {
return $this->excluded;
}
/**
* @return string[]
*/
public function getRequired() {
return $this->required;
}
/**
* @return bool
*/
public function getRequiredAll() {
$config_orig = $this->getConfigOrig();
$info = is_array($this->required) ? $this->required : array();
$diff = array_diff($config_orig, $info);
// Mark all as required if required:true, or required is empty, or
// if required contains all the exported config
return empty($diff) || empty($info);
}
/**
* @return string[]
*/
public function getConfigOrig() {
return $this->configOrig;
}
/**
* @return string
*/
public function getCore() {
return $this->core;
}
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @return \string[]
*/
public function getThemes() {
return $this->themes;
}
/**
* @return array
*/
public function getInfo() {
return $this->info;
}
/**
* @return mixed
*/
public function getState() {
return $this->state;
}
/**
* @return string
*/
public function getDirectory() {
return $this->directory;
}
/**
* @return mixed
*/
public function getFiles() {
return $this->files;
}
/**
* @return \Drupal\Core\Extension\Extension
*/
public function getExtension() {
return $this->extension;
}
public function getDependencies() {
return $this->dependencies;
}
public function removeDependency($name) {
$this->dependencies = array_diff($this->dependencies, [$name]);
return $this;
}
public function getDependencyInfo() {
return isset($this->info['dependencies']) ? $this->info['dependencies'] : [];
}
/**
* Returns the features info.
*
* @return array
*/
public function getFeaturesInfo() {
$info = [];
if (!empty($this->bundle)) {
$info['bundle'] = $this->bundle;
}
if (!empty($this->excluded)) {
$info['excluded'] = $this->excluded;
}
if ($this->required !== FALSE) {
$info['required'] = $this->required;
}
return $info;
}
/**
* Sets a new machine name.
*
* @param string $machine_name
* The machine name
*
* @return $this
*/
public function setMachineName($machine_name) {
$this->machineName = $machine_name;
return $this;
}
/**
* @param string $name
*
* @return $this
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* @param string $description
*
* @return $this
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
/**
* @param string $version
*
* @return $this
*/
public function setVersion($version) {
$this->version = $version;
return $this;
}
/**
* @param string $bundle
*
* @return $this
*/
public function setBundle($bundle) {
$this->bundle = $bundle;
return $this;
}
/**
* @param array $info
*
* @return $this
*/
public function setInfo($info) {
$this->info = $info;
return $this;
}
/**
* @param \string[] $features_info
*
* @return $this
*/
public function setFeaturesInfo($features_info) {
if (isset($features_info['bundle'])) {
$this->setBundle($features_info['bundle']);
}
$this->setRequired(isset($features_info['required']) ? $features_info['required'] : false);
$this->setExcluded(isset($features_info['excluded']) ? $features_info['excluded'] : array());
return $this;
}
/**
* @param \string[] $dependencies
*
* @return $this
*/
public function setDependencies($dependencies) {
$this->dependencies = $dependencies;
return $this;
}
/**
* @param string $dependency
*
* return $this
*/
public function appendDependency($dependency) {
$this->dependencies[] = $dependency;
return $this;
}
/**
* @param int $status
*
* @return $this
*/
public function setStatus($status) {
$this->status = $status;
return $this;
}
/**
* @param \string[] $config
*
* @return $this
*/
public function setConfig($config) {
$this->config = $config;
return $this;
}
/**
* @param bool $excluded
*/
public function setExcluded($excluded) {
$this->excluded = $excluded;
}
/**
* @param bool $required
*/
public function setRequired($required) {
$this->required = $required;
}
/**
* @param string $core
*/
public function setCore($core) {
$this->core = $core;
}
/**
* @param string $type
*/
public function setType($type) {
$this->type = $type;
}
/**
* @param \string[] $themes
*/
public function setThemes($themes) {
$this->themes = $themes;
}
/**
* @param int $state
*/
public function setState($state) {
$this->state = $state;
}
/**
* @param string $directory
*/
public function setDirectory($directory) {
$this->directory = $directory;
}
/**
* @param \string[] $files
*/
public function setFiles($files) {
$this->files = $files;
}
/**
* @param array $file_array
*
* @return $this
*/
public function appendFile(array $file_array, $key = NULL) {
if (!isset($key)) {
$this->files[] = $file_array;
}
else {
$this->files[$key] = $file_array;
}
return $this;
}
/**
* @param \Drupal\Core\Extension\Extension $extension
*/
public function setExtension($extension) {
$this->extension = $extension;
}
/**
* @param \string[] $configOrig
*/
public function setConfigOrig($configOrig) {
$this->configOrig = $configOrig;
}
}
@@ -0,0 +1,69 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\component\Utility\Unicode;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to packages based on entity types.
*
* @Plugin(
* id = "base",
* weight = -2,
* name = @Translation("Base type"),
* description = @Translation("Use designated types of configuration as the base for configuration package modules. For example, if content types are selected as a base type, a package will be generated for each content type and will include all configuration dependent on that content type."),
* config_route_name = "features.assignment_base",
* default_settings = {
* "types" = {
* "config" = {},
* "content" = {}
* }
* }
* )
*/
class FeaturesAssignmentBaseType extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$current_bundle = $this->assigner->getBundle();
$settings = $current_bundle->getAssignmentSettings($this->getPluginId());
$config_base_types = $settings['types']['config'];
$config_types = $this->featuresManager->listConfigTypes();
$config_collection = $this->featuresManager->getConfigCollection();
foreach ($config_collection as $item_name => $item) {
if (in_array($item->getType(), $config_base_types)) {
if (is_null($this->featuresManager->findPackage($item->getShortName())) && !$item->getPackage()) {
$description = $this->t('Provides @label @type and related configuration.', array('@label' => $item->getLabel(), '@type' => Unicode::strtolower($config_types[$item->getType()])));
if (isset($item->getData()['description'])) {
$description .= ' ' . $item->getData()['description'];
}
$this->featuresManager->initPackage($item->getShortName(), $item->getLabel(), $description, 'module', $current_bundle);
// Update list with the package we just added.
try {
$this->featuresManager->assignConfigPackage($item->getShortName(), [$item_name]);
}
catch (\Exception $exception) {
\Drupal::logger('features')->error($exception->getMessage());
}
$this->featuresManager->assignConfigDependents([$item_name]);
}
}
}
$entity_types = $this->entityManager->getDefinitions();
$content_base_types = $settings['types']['content'];
foreach ($content_base_types as $entity_type_id) {
if (!isset($packages[$entity_type_id]) && isset($entity_types[$entity_type_id])) {
$label = $entity_types[$entity_type_id]->getLabel();
$description = $this->t('Provide @label related configuration.', array('@label' => $label));
$this->featuresManager->initPackage($entity_type_id, $label, $description, 'module', $current_bundle);
}
}
}
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to a core package based on entity types.
*
* @Plugin(
* id = "core",
* weight = 5,
* name = @Translation("Core type"),
* description = @Translation("Assign designated types of configuration to a core configuration package module. For example, if image styles are selected as a core type, a core package will be generated and image styles will be assigned to it."),
* config_route_name = "features.assignment_core",
* default_settings = {
* "types" = {
* "config" = {},
* }
* }
* )
*/
class FeaturesAssignmentCoreType extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$current_bundle = $this->assigner->getBundle();
$machine_name = 'core';
$name = $this->t('Core');
$description = $this->t('Provides core components required by other features.');
$this->featuresManager->initPackage($machine_name, $name, $description, 'module', $current_bundle);
$this->assignPackageByConfigTypes($machine_name, $force);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to packages based on configuration
* dependencies.
*
* @Plugin(
* id = "dependency",
* weight = 15,
* name = @Translation("Dependency"),
* description = @Translation("Add to packages configuration dependent on items already in that package."),
* )
*/
class FeaturesAssignmentDependency extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$this->featuresManager->assignConfigDependents();
}
}
@@ -0,0 +1,148 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for excluding configuration from packages.
*
* @Plugin(
* id = "exclude",
* weight = -5,
* name = @Translation("Exclude"),
* description = @Translation("Exclude configuration items from packaging by various methods including by configuration type."),
* config_route_name = "features.assignment_exclude",
* default_settings = {
* "curated" = FALSE,
* "module" = {
* "installed" = FALSE,
* "profile" = FALSE,
* "namespace" = FALSE,
* "namespace_any" = FALSE,
* },
* "types" = { "config" = {} }
* }
* )
*/
class FeaturesAssignmentExclude extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$current_bundle = $this->assigner->getBundle();
$settings = $current_bundle->getAssignmentSettings($this->getPluginId());
$config_collection = $this->featuresManager->getConfigCollection();
// Exclude by configuration type.
$exclude_types = $settings['types']['config'];
if (!empty($exclude_types)) {
foreach ($config_collection as $item_name => $item) {
// Don't exclude already-assigned items.
if (empty($item->getPackage()) && in_array($item->getType(), $exclude_types)) {
$item->setExcluded(TRUE);
}
}
}
// Exclude configuration already provided by modules.
$exclude_module = $settings['module'];
if (!empty($exclude_module['installed'])) {
$install_list = $this->featuresManager->getExtensionStorages()->listAll();
// There are two settings that can limit what's included.
// First, we can skip configuration provided by the install profile.
$module_profile = !empty($exclude_module['profile']);
// Second, we can skip configuration provided by namespaced modules.
$module_namespace = !empty($exclude_module['namespace']);
if ($module_profile || $module_namespace) {
$profile_list = [];
$extension_list = [];
// Load the names of any configuration objects provided by the install
// profile.
if ($module_profile) {
$all_modules = $this->featuresManager->getAllModules();
// FeaturesBundleInterface::getProfileName() would return the profile
// for the current bundle, if any. We want the profile that was
// installed.
$profile_name = drupal_get_profile();
if (isset($all_modules[$profile_name])) {
$profile_list = $this->featuresManager->listExtensionConfig($all_modules[$profile_name]);
// If the configuration has been assigned to a feature that's
// present on the file system, don't make an exception for it.
foreach ($all_modules as $name => $extension) {
if ($name != $profile_name && $this->featuresManager->isFeatureModule($extension)) {
$profile_list = array_diff($profile_list, $this->featuresManager->listExtensionConfig($extension));
}
}
}
}
// Load the names of any configuration objects provided by modules
// having the namespace of the current package set.
if ($module_namespace) {
$modules = $this->featuresManager->getFeaturesModules($current_bundle);
foreach ($modules as $extension) {
// Only make exception for non-exported modules
if (!empty($exclude_module['namespace_any']) || !isset($all_modules[$extension->getName()])) {
$extension_list = array_merge($extension_list, $this->featuresManager->listExtensionConfig($extension));
}
}
}
// If any configuration was found, remove it from the list.
$install_list = array_diff($install_list, $profile_list, $extension_list);
}
foreach ($install_list as $item_name) {
if (isset($config_collection[$item_name])) {
// Flag extension-provided configuration, which should not be added
// to regular features but can be added to an install profile.
$config_collection[$item_name]->setProviderExcluded(TRUE);
}
}
}
// Exclude configuration items on a curated list of site-specific
// configuration.
if ($settings['curated']) {
$item_names = [
'core.extension',
'field.settings',
'field_ui.settings',
'filter.settings',
'forum.settings',
'image.settings',
'node.settings',
'system.authorize',
'system.date',
'system.file',
'system.diff',
'system.logging',
'system.maintenance',
'system.performance',
'system.site',
'update.settings',
];
foreach ($item_names as $item_name) {
unset($config_collection[$item_name]);
}
// Unset role-related actions that are automatically created by the
// User module.
// @see user_user_role_insert()
$prefixes = [
'system.action.user_add_role_action.',
'system.action.user_remove_role_action.',
];
foreach (array_keys($config_collection) as $item_name) {
foreach ($prefixes as $prefix) {
if (strpos($item_name, $prefix) === 0) {
unset($config_collection[$item_name]);
}
}
}
}
// Register the updated data.
$this->featuresManager->setConfigCollection($config_collection);
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
use Drupal\features\FeaturesManagerInterface;
/**
* Class for assigning existing modules to packages.
*
* @Plugin(
* id = "existing",
* weight = 12,
* name = @Translation("Existing"),
* description = @Translation("Add exported config to existing packages."),
* )
*/
class FeaturesAssignmentExisting extends FeaturesAssignmentMethodBase {
/**
* Calls assignConfigPackage without allowing exceptions to abort us.
*
* @param string $machine_name
* Machine name of package.
* @param \Drupal\Core\Extension\Extension $extension
* An Extension object.
*/
protected function safeAssignConfig($machine_name, $extension) {
$config = $this->featuresManager->listExtensionConfig($extension);
try {
$this->featuresManager->assignConfigPackage($machine_name, $config);
}
catch (\Exception $exception) {
\Drupal::logger('features')->error($exception->getMessage());
}
}
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$packages = $this->featuresManager->getPackages();
// Assign config to installed modules first.
foreach ($packages as $name => $package) {
// @todo Introduce $package->isInstalled() and / or $package->isUninstalled().
if ($package->getStatus() === FeaturesManagerInterface::STATUS_INSTALLED) {
$this->safeAssignConfig($package->getMachineName(), $package->getExtension());
}
}
// Now assign to uninstalled modules.
foreach ($packages as $name => $package) {
if ($package->getStatus() === FeaturesManagerInterface::STATUS_UNINSTALLED) {
$this->safeAssignConfig($package->getMachineName(), $package->getExtension());
}
}
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\Component\Graph\Graph;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to packages based on forward dependencies.
*
* @Plugin(
* id = "forward_dependency",
* weight = 20,
* name = @Translation("Forward dependency"),
* description = @Translation("Add to packages configuration on which items in the package depend."),
* )
*/
class FeaturesAssignmentForwardDependency extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$config_collection = $this->featuresManager->getConfigCollection();
$ordered = $this->dependencyOrder($config_collection);
foreach ($ordered as $name) {
$item = $config_collection[$name];
if ($item->getPackage()) {
// Already has a package, not our business.
continue;
}
// Find packages of dependent items.
$dependent_packages = [];
foreach ($item->getDependents() as $dependent) {
if (isset($config_collection[$dependent])) {
if ($package = $config_collection[$dependent]->getPackage()) {
$dependent_packages[$package] = $package;
}
}
}
// If zero or multiple packages, we don't know what to do.
if (count($dependent_packages) == 1) {
$package = key($dependent_packages);
$this->featuresManager->assignConfigPackage($package, [$name]);
}
}
}
/**
* Get config items such that each item comes before anything it depends on.
*
* @param \Drupal\features\ConfigurationItem[] $config_collection
* A collection of configuration items.
*
* @return string[]
* The names of configuration items, in dependency order.
*/
protected function dependencyOrder($config_collection) {
// Populate a graph.
$graph = [];
foreach ($config_collection as $config) {
$graph[$config->getName()] = [];
foreach ($config->getDependents() as $dependent) {
$graph[$config->getName()]['edges'][$dependent] = 1;
}
}
$graph_object = new Graph($graph);
$graph = $graph_object->searchAndSort();
// Order by inverse weight.
$weights = array_column($graph, 'weight');
array_multisort($weights, SORT_DESC, $graph);
return array_keys($graph);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to packages based on namespaces.
*
* @Plugin(
* id = "namespace",
* weight = 0,
* name = @Translation("Namespace"),
* description = @Translation("Add to packages configuration with a machine name containing that package's machine name."),
* )
*/
class FeaturesAssignmentNamespace extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$packages = array_keys($this->featuresManager->getPackages());
$this->featuresManager->assignConfigByPattern(array_combine($packages, $packages));
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\Core\Config\InstallStorage;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to the
* InstallStorage::CONFIG_OPTIONAL_DIRECTORY based on entity types.
*
* @Plugin(
* id = "optional",
* weight = 0,
* name = @Translation("Optional type"),
* description = @Translation("Assign designated types of configuration to the 'config/optional' install directory. For example, if views are selected as optional, views assigned to any feature will be exported to the 'config/optional' directory and will not create a dependency on the Views module."),
* config_route_name = "features.assignment_optional",
* default_settings = {
* "types" = {
* "config" = {},
* }
* }
* )
*/
class FeaturesAssignmentOptionalType extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$this->assignSubdirectoryByConfigTypes(InstallStorage::CONFIG_OPTIONAL_DIRECTORY);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning existing modules to packages.
*
* @Plugin(
* id = "packages",
* weight = -20,
* name = @Translation("Packages"),
* description = @Translation("Detect and add existing package modules."),
* )
*/
class FeaturesAssignmentPackages extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$bundle = $this->assigner->getBundle();
$existing = $this->featuresManager->getFeaturesModules();
foreach ($existing as $extension) {
$package = $this->featuresManager->initPackageFromExtension($extension);
$short_name = $package->getMachineName();
// Copy over package excluded settings, if any.
if (!$package->getExcluded()) {
$config_collection = $this->featuresManager->getConfigCollection();
foreach ($package->getExcluded() as $config_name) {
if (isset($config_collection[$config_name])) {
$package_excluded = $config_collection[$config_name]->getPackageExcluded();
$package_excluded[] = $short_name;
$config_collection[$config_name]->setPackageExcluded($package_excluded);
}
}
$this->featuresManager->setConfigCollection($config_collection);
}
// Assign required components, if any.
if ($package->getRequired() !== FALSE) {
$config = $package->getRequired();
if (empty($config) || !is_array($config)) {
// if required is "true" or empty, add all config as required
$config = $this->featuresManager->listExtensionConfig($extension);
}
$this->featuresManager->assignConfigPackage($short_name, $config);
}
}
}
}
@@ -0,0 +1,177 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
use Drupal\Core\Config\InstallStorage;
/**
* Class for adding configuration for the optional install profile.
*
* @Plugin(
* id = "profile",
* weight = 10,
* name = @Translation("Profile"),
* description = @Translation("Add configuration and other files to the optional install profile from the Drupal core Standard install profile. Without these additions, a generated install profile will be missing some important initial setup."),
* config_route_name = "features.assignment_profile",
* default_settings = {
* "curated" = FALSE,
* "standard" = {
* "files" = FALSE,
* "dependencies" = FALSE,
* },
* "types" = { "config" = {} }
* }
* )
*/
class FeaturesAssignmentProfile extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$current_bundle = $this->assigner->getBundle();
if ($current_bundle->isProfile()) {
$settings = $current_bundle->getAssignmentSettings($this->getPluginId());
// Ensure the profile package exists.
$profile_name = $current_bundle->getProfileName();
$profile_package = $this->featuresManager->getPackage($profile_name);
if (empty($profile_package)) {
$profile_package = $this->featuresManager->initPackage($profile_name, $current_bundle->getName(), $current_bundle->getDescription(), 'profile', $current_bundle);
}
// Assign configuration by type.
$this->assignPackageByConfigTypes($profile_name, $force);
// Include a curated list of configuration.
if ($settings['curated']) {
$config_collection = $this->featuresManager->getConfigCollection();
$item_names = [
'automated_cron.settings',
'system.cron',
'system.theme',
];
$theme_settings = $this->configFactory->get('system.theme');
foreach (['default', 'admin'] as $key) {
$item_names[] = $theme_settings->get($key) . '.settings';
}
foreach ($item_names as $item_name) {
if (isset($config_collection[$item_name])) {
try {
$this->featuresManager->assignConfigPackage($profile_name, [$item_name]);
}
catch (\Exception $exception) {
\Drupal::logger('features')->error($exception->getMessage());
}
}
}
}
// Only read in from the Standard profile if this profile doesn't already
// exist.
$package_directories = $this->featuresManager->listPackageDirectories(array(), $current_bundle);
if (!isset($package_directories[$profile_name])) {
$standard_directory = 'core/profiles/standard';
// Conditionally add files from the 'standard' install profile.
if ($settings['standard']['files']) {
// Add configuration from the Standard profile.
$config_collection = $this->featuresManager->getConfigCollection();
$subdirectory = InstallStorage::CONFIG_INSTALL_DIRECTORY;
$item_names = $this->listRequiredStandardConfig();
foreach ($item_names as $item_name) {
// If the configuration is present on the site, assign it.
if (isset($config_collection[$item_name])) {
// Only assign it if it's not already assigned to a package.
// @todo: if it's provided by a module, add a dependency.
if (!$config_collection[$item_name]->getPackage()) {
$this->featuresManager->assignConfigPackage($profile_name, [$item_name], $force);
// Reload the profile to refresh the config array after the addition.
$profile_package = $this->featuresManager->getPackage($profile_name);
}
// If it's already assigned to a package in the current bundle,
// add a dependency.
else {
$machine_name = $current_bundle->getFullName($config_collection[$item_name]->getPackage());
if (!in_array($machine_name, $profile_package->getDependencies())) {
$profile_package->appendDependency($machine_name);
}
}
}
// Otherwise, copy it over from Standard.
else {
$filename = $item_name . '.yml';
$profile_package->appendFile([
'filename' => $filename,
'subdirectory' => $subdirectory,
'string' => file_get_contents($standard_directory . '/' . $subdirectory . '/' . $filename)
]);
}
}
// Add .profile and .install files from Standard.
$files = [
'install',
'profile',
];
// Iterate through the files.
foreach ($files as $extension) {
$filename = $standard_directory . '/standard.' . $extension;
if (file_exists($filename)) {
// Read the file contents.
$string = file_get_contents($filename);
// Substitute the profile's machine name and name for the Standard
// profile's equivalents.
$string = str_replace(
['standard', 'Standard'],
[$profile_name, $current_bundle->getName()],
$string
);
// Add the files to those to be output.
$profile_package->appendFile([
'filename' => $profile_name . '.' . $extension,
'subdirectory' => NULL,
'string' => $string
], $extension);
}
}
}
// Conditionally merge in module and theme dependencies from the
// 'standard' install profile.
if ($settings['standard']['dependencies']) {
$info_file_uri = $standard_directory . '/standard.info.yml';
if (file_exists($info_file_uri)) {
$profile_info = \Drupal::service('info_parser')->parse($info_file_uri);
$info = [
'dependencies' => $profile_package->getDependencies(),
'themes' => $profile_package->getThemes(),
];
$info = $this->featuresManager->mergeInfoArray($info, $profile_info);
$profile_package->setDependencies($info['dependencies']);
$profile_package->setThemes($info['themes']);
}
}
$this->featuresManager->setPackage($profile_package);
}
}
}
/**
* Returns the list of configuration items required by the Standard install
* profile.
*
* If install code is adapted from the Standard profile, these configuration
* items will be required.
*
* @return array
* An array of configuration item names.
*/
protected function listRequiredStandardConfig() {
return [
'contact.form.feedback',
'user.role.administrator'
];
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\features\Plugin\FeaturesAssignment;
use Drupal\features\FeaturesAssignmentMethodBase;
/**
* Class for assigning configuration to a site package based on entity types.
*
* @Plugin(
* id = "site",
* weight = 7,
* name = @Translation("Site type"),
* description = @Translation("Assign designated types of configuration to a site configuration package module. For example, if image styles are selected as a site type, a site package will be generated and image styles will be assigned to it."),
* config_route_name = "features.assignment_site",
* default_settings = {
* "types" = {
* "config" = {},
* }
* }
* )
*/
class FeaturesAssignmentSiteType extends FeaturesAssignmentMethodBase {
/**
* {@inheritdoc}
*/
public function assignPackages($force = FALSE) {
$current_bundle = $this->assigner->getBundle();
$machine_name = 'site';
$name = $this->t('Site');
$description = $this->t('Provides site components.');
$this->featuresManager->initPackage($machine_name, $name, $description, 'module', $current_bundle);
$this->assignPackageByConfigTypes($machine_name, $force);
}
}
@@ -0,0 +1,270 @@
<?php
namespace Drupal\features\Plugin\FeaturesGeneration;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\features\FeaturesGenerationMethodBase;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Form\FormStateInterface;
use Drupal\features\FeaturesBundleInterface;
use Drupal\features\Package;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class for generating a compressed archive of packages.
*
* @Plugin(
* id = \Drupal\features\Plugin\FeaturesGeneration\FeaturesGenerationArchive::METHOD_ID,
* weight = -2,
* name = @Translation("Download Archive"),
* description = @Translation("Generate packages and optional profile as a compressed archive for download."),
* )
*/
class FeaturesGenerationArchive extends FeaturesGenerationMethodBase implements ContainerFactoryPluginInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Creates a new FeaturesGenerationArchive instance.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
public function __construct(\Drupal\Core\Access\CsrfTokenGenerator $csrf_token) {
$this->csrfToken = $csrf_token;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('csrf_token')
);
}
/**
* The package generation method id.
*/
const METHOD_ID = 'archive';
/**
* The filename being written.
*
* @var string
*/
protected $archiveName;
/**
* Reads and merges in existing files for a given package or profile.
*/
protected function preparePackage(Package $package, array $existing_packages, FeaturesBundleInterface $bundle = NULL) {
if (isset($existing_packages[$package->getMachineName()])) {
$existing_directory = $existing_packages[$package->getMachineName()];
// Scan for all files.
$files = file_scan_directory($existing_directory, '/.*/');
foreach ($files as $file) {
// Skip files in the any existing configuration directory, as these
// will be replaced.
foreach (array_keys($this->featuresManager->getExtensionStorages()->getExtensionStorages()) as $directory) {
if (strpos($file->uri, $directory) !== FALSE) {
continue 2;
}
}
// Merge in the info file.
if ($file->name == $package->getMachineName() . '.info') {
$files = $package->getFiles();
$files['info']['string'] = $this->mergeInfoFile($package->getFiles()['info']['string'], $file->uri);
$package->setFiles($files);
}
// Read in remaining files.
else {
// Determine if the file is within a subdirectory of the
// extension's directory.
$file_directory = dirname($file->uri);
if ($file_directory !== $existing_directory) {
$subdirectory = substr($file_directory, strlen($existing_directory) + 1);
}
else {
$subdirectory = NULL;
}
$package->appendFile([
'filename' => $file->filename,
'subdirectory' => $subdirectory,
'string' => file_get_contents($file->uri)
]);
}
}
}
}
/**
* {@inheritdoc}
*/
public function generate(array $packages = array(), FeaturesBundleInterface $bundle = NULL) {
// If no packages were specified, get all packages.
if (empty($packages)) {
$packages = $this->featuresManager->getPackages();
}
// Determine the best name for the tar archive.
// Single package export, so name by package name.
if (count($packages) == 1) {
$filename = current($packages)->getMachineName();
}
// Profile export, so name by profile.
elseif (isset($bundle) && $bundle->isProfile()) {
$filename = $bundle->getProfileName();
}
// Non-default bundle, so name by bundle.
elseif (isset($bundle) && !$bundle->isDefault()) {
$filename = $bundle->getMachineName();
}
// Set a fallback name.
else {
$filename = 'generated_features';
}
$return = [];
$this->archiveName = $filename . '.tar.gz';
$archive_name = file_directory_temp() . '/' . $this->archiveName;
if (file_exists($archive_name)) {
file_unmanaged_delete($archive_name);
}
$archiver = new ArchiveTar($archive_name);
// Add package files.
foreach ($packages as $package) {
if (count($packages) == 1) {
// Single module export, so don't generate entire modules dir structure.
$package->setDirectory($package->getMachineName());
}
$this->generatePackage($return, $package, $archiver);
}
return $return;
}
/**
* Writes a package or profile's files to an archive.
*
* @param array &$return
* The return value, passed by reference.
* @param \Drupal\features\Package $package
* The package or profile.
* @param ArchiveTar $archiver
* The archiver.
*/
protected function generatePackage(array &$return, Package $package, ArchiveTar $archiver) {
$success = TRUE;
foreach ($package->getFiles() as $file) {
try {
$this->generateFile($package->getDirectory(), $file, $archiver);
}
catch (\Exception $exception) {
$this->failure($return, $package, $exception);
$success = FALSE;
break;
}
}
if ($success) {
$this->success($return, $package);
}
}
/**
* Registers a successful package or profile archive operation.
*
* @param array &$return
* The return value, passed by reference.
* @param \Drupal\features\Package $package
* The package or profile.
*/
protected function success(array &$return, Package $package) {
$type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile');
$return[] = [
'success' => TRUE,
// Archive writing doesn't merit a message, and if done through the UI
// would appear on the subsequent page load.
'display' => FALSE,
'message' => '@type @package written to archive.',
'variables' => [
'@type' => $type,
'@package' => $package->getName(),
],
];
}
/**
* Registers a failed package or profile archive operation.
*
* @param array &$return
* The return value, passed by reference.
* @param \Drupal\features\Package $package
* The package or profile.
* @param \Exception $exception
* The exception object.
* @param string $message
* Error message when there isn't an Exception object.
*/
protected function failure(array &$return, Package $package, \Exception $exception = NULL, $message = '') {
$type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile');
$return[] = [
'success' => FALSE,
// Archive writing doesn't merit a message, and if done through the UI
// would appear on the subsequent page load.
'display' => FALSE,
'message' => '@type @package not written to archive. Error: @error.',
'variables' => [
'@type' => $type,
'@package' => $package->getName(),
'@error' => isset($exception) ? $exception->getMessage() : $message,
],
];
}
/**
* Writes a file to the file system, creating its directory as needed.
*
* @param string $directory
* The extension's directory.
* @param array $file
* Array with the following keys:
* - 'filename': the name of the file.
* - 'subdirectory': any subdirectory of the file within the extension
* directory.
* - 'string': the contents of the file.
* @param ArchiveTar $archiver
* The archiver.
*
* @throws Exception
*/
protected function generateFile($directory, array $file, ArchiveTar $archiver) {
$filename = $directory;
if (!empty($file['subdirectory'])) {
$filename .= '/' . $file['subdirectory'];
}
$filename .= '/' . $file['filename'];
// Set the mode to 0644 rather than the default of 0600.
if ($archiver->addString($filename, $file['string'], FALSE, ['mode' => 0644]) === FALSE) {
throw new \Exception($this->t('Failed to archive file @filename.', ['@filename' => $file['filename']]));
}
}
/**
* {@inheritdoc}
*/
public function exportFormSubmit(array &$form, FormStateInterface $form_state) {
// Redirect to the archive file download.
$form_state->setRedirect('features.export_download', ['uri' => $this->archiveName, 'token' => $this->csrfToken->get($this->archiveName)]);
}
}
@@ -0,0 +1,225 @@
<?php
namespace Drupal\features\Plugin\FeaturesGeneration;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\features\FeaturesGenerationMethodBase;
use Drupal\features\FeaturesBundleInterface;
use Drupal\features\Package;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class for writing packages to the local file system.
*
* @Plugin(
* id = \Drupal\features\Plugin\FeaturesGeneration\FeaturesGenerationWrite::METHOD_ID,
* weight = 2,
* name = @Translation("Write"),
* description = @Translation("Write packages and optional profile to the file system."),
* )
*/
class FeaturesGenerationWrite extends FeaturesGenerationMethodBase implements ContainerFactoryPluginInterface {
/**
* The package generation method id.
*/
const METHOD_ID = 'write';
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Creates a new FeaturesGenerationWrite instance.
*
* @param string $root
* The app root.
*/
public function __construct($root) {
$this->root = $root;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('app.root')
);
}
/**
* Reads and merges in existing files for a given package or profile.
*
* @param \Drupal\features\Package &$package
* The package.
* @param array $existing_packages
* An array of existing packages.
* @param \Drupal\features\FeaturesBundleInterface $bundle
* The bundle the package belongs to.
*/
protected function preparePackage(Package $package, array $existing_packages, FeaturesBundleInterface $bundle = NULL) {
// If this package is already present, prepare files.
if (isset($existing_packages[$package->getMachineName()])) {
$existing_directory = $existing_packages[$package->getMachineName()];
$package->setDirectory($existing_directory);
// Merge in the info file.
$info_file_uri = $this->root . '/' . $existing_directory . '/' . $package->getMachineName() . '.info.yml';
if (file_exists($info_file_uri)) {
$files = $package->getFiles();
$files['info']['string'] = $this->mergeInfoFile($package->getFiles()['info']['string'], $info_file_uri);
$package->setFiles($files);
}
// Remove the config directories, as they will be replaced.
foreach (array_keys($this->featuresManager->getExtensionStorages()->getExtensionStorages()) as $directory) {
$config_directory = $this->root . '/' . $existing_directory . '/' . $directory;
if (is_dir($config_directory)) {
file_unmanaged_delete_recursive($config_directory);
}
}
}
}
/**
* {@inheritdoc}
*/
public function generate(array $packages = array(), FeaturesBundleInterface $bundle = NULL) {
// If no packages were specified, get all packages.
if (empty($packages)) {
$packages = $this->featuresManager->getPackages();
}
$return = [];
// Add package files.
// We need to update the system.module.files state because it's cached.
// Cannot just call system_rebuild_module_data() because $listing->scan() has
// it's own internal static cache that we cannot clear at this point.
$files = \Drupal::state()->get('system.module.files');
foreach ($packages as $package) {
$this->generatePackage($return, $package);
if (!isset($files[$package->getMachineName()]) && isset($package->getFiles()['info'])) {
$files[$package->getMachineName()] = $package->getDirectory() . '/' . $package->getFiles()['info']['filename'];
}
}
// Rebuild system module cache
\Drupal::state()->set('system.module.files', $files);
return $return;
}
/**
* Writes a package or profile's files to the file system.
*
* @param array &$return
* The return value, passed by reference.
* @param \Drupal\features\Package $package
* The package or profile.
*/
protected function generatePackage(array &$return, Package $package) {
if (!$package->getFiles()) {
$this->failure($return, $package, NULL, $this->t('No configuration was selected to be exported.'));
return;
}
$success = TRUE;
foreach ($package->getFiles() as $file) {
try {
$this->generateFile($package->getDirectory(), $file);
}
catch (\Exception $exception) {
$this->failure($return, $package, $exception);
$success = FALSE;
break;
}
}
if ($success) {
$this->success($return, $package);
}
}
/**
* Registers a successful package or profile write operation.
*
* @param array &$return
* The return value, passed by reference.
* @param \Drupal\features\Package $package
* The package or profile.
*/
protected function success(array &$return, Package $package) {
$type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile');
$return[] = [
'success' => TRUE,
'display' => TRUE,
'message' => '@type @package written to @directory.',
'variables' => [
'@type' => $type,
'@package' => $package->getName(),
'@directory' => $package->getDirectory(),
],
];
}
/**
* Registers a failed package or profile write operation.
*
* @param array &$return
* The return value, passed by reference.
* @param \Drupal\features\Package $package
* The package or profile.
* @param \Exception $exception
* The exception object.
* @param string $message
* Error message when there isn't an Exception object.
*/
protected function failure(array &$return, Package $package, \Exception $exception = NULL, $message = '') {
$type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile');
$return[] = [
'success' => FALSE,
'display' => TRUE,
'message' => '@type @package not written to @directory. Error: @error.',
'variables' => [
'@type' => $type,
'@package' => $package->getName(),
'@directory' => $package->getDirectory(),
'@error' => isset($exception) ? $exception->getMessage() : $message,
],
];
}
/**
* Writes a file to the file system, creating its directory as needed.
*
* @param string $directory
* The extension's directory.
* @param array $file
* Array with the following keys:
* - 'filename': the name of the file.
* - 'subdirectory': any subdirectory of the file within the extension
* directory.
* - 'string': the contents of the file.
*
* @throws Exception
*/
protected function generateFile($directory, array $file) {
if (!empty($file['subdirectory'])) {
$directory .= '/' . $file['subdirectory'];
}
$directory = $this->root . '/' . $directory;
if (!is_dir($directory)) {
if (drupal_mkdir($directory, NULL, TRUE) === FALSE) {
throw new \Exception($this->t('Failed to create directory @directory.', ['@directory' => $directory]));
}
}
if (file_put_contents($directory . '/' . $file['filename'], $file['string']) === FALSE) {
throw new \Exception($this->t('Failed to write file @filename.', ['@filename' => $file['filename']]));
}
}
}
@@ -0,0 +1,136 @@
<?php
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\features\FeaturesConfigInstaller' "modules/contrib/features/src".
*/
namespace Drupal\features\ProxyClass {
/**
* Provides a proxy class for \Drupal\features\FeaturesConfigInstaller.
*
* @see \Drupal\Component\ProxyBuilder
*/
class FeaturesConfigInstaller implements \Drupal\Core\Config\ConfigInstallerInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\features\FeaturesConfigInstaller
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
/**
* {@inheritdoc}
*/
public function installDefaultConfig($type, $name)
{
return $this->lazyLoadItself()->installDefaultConfig($type, $name);
}
/**
* {@inheritdoc}
*/
public function installOptionalConfig(\Drupal\Core\Config\StorageInterface $storage = NULL, $dependency = array (
))
{
return $this->lazyLoadItself()->installOptionalConfig($storage, $dependency);
}
/**
* {@inheritdoc}
*/
public function installCollectionDefaultConfig($collection)
{
return $this->lazyLoadItself()->installCollectionDefaultConfig($collection);
}
/**
* {@inheritdoc}
*/
public function setSourceStorage(\Drupal\Core\Config\StorageInterface $storage)
{
return $this->lazyLoadItself()->setSourceStorage($storage);
}
/**
* {@inheritdoc}
*/
public function getSourceStorage()
{
return $this->lazyLoadItself()->getSourceStorage();
}
/**
* {@inheritdoc}
*/
public function setSyncing($status)
{
return $this->lazyLoadItself()->setSyncing($status);
}
/**
* {@inheritdoc}
*/
public function isSyncing()
{
return $this->lazyLoadItself()->isSyncing();
}
/**
* {@inheritdoc}
*/
public function checkConfigurationToInstall($type, $name)
{
return $this->lazyLoadItself()->checkConfigurationToInstall($type, $name);
}
}
}