Problem/Motivation

During the development of a site, developers should almost always use the same custom bundle to export features related to that site. This means that every time they export a feature (at least between sessions) they have to make sure to manually select the correct bundle.

Steps to reproduce

  1. Create a new bundle and select it in the "Bundle" field on the features overview page (/admin/config/development/features).
  2. When you re-login and go to the features overview page then you should see that created bundle is still selected.

Proposed resolution

One option would be if the "default" bundle could be saved as a config value. Note that this is not the "default" bundle that's provided out of the box by Features, but rather the ability to set any bundle as the default bundle.

However, there are issues with that approach. We currently store the default bundle in a session variable. If we add a new default setting to features.settings, we have to map out the interaction between the session variable and the new setting. Also, we have to set the initial value to default (since, in Features itself, we can't count on any other value being present).

Alternatives:

  • When a new features bundle is installed, set it as the current bundle in the session. Downside: this will work only for the user who installed the bundle.
  • Move the current bundle out of the session and instead use a state setting. Downside: all users would have the same default bundle.

Instead:

  • Use state (per the state API) in addition to the session to store the current bundle. First check session, then state, and if both are empty then fall back to default.
  • When a new feature bundle is installed, set it in the state.

Issue fork features-2789001

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Dane Powell created an issue. See original summary.

nedjo’s picture

Issue summary: View changes
nedjo’s picture

Agreed this is an issue worth addressing. Looked a bit at options and found it's more complex than it first appears. Sketched in some possibilities.

nedjo’s picture

Issue summary: View changes
nedjo’s picture

Issue summary: View changes
nedjo’s picture

Status: Active » Needs review
StatusFileSize
new6.73 KB

Patch with test.

  • nedjo committed 6e973b8 on 8.x-3.x
    Issue #2789001 by nedjo: Set default bundle for site
    
nedjo’s picture

Status: Needs review » Fixed

  • nedjo committed 96e978a on 8.x-3.x
    Issue #2789001 by nedjo: revert previous commit as it can trigger a...
nedjo’s picture

Status: Fixed » Active

Reverted the previous commit as it triggered #2948432: Circular reference detected for service "features.config_save_subscriber".

Setting back to active.

norman.lol’s picture

Hey hey, can you outline roughly what exactly caused this problem?

chmez made their first commit to this issue’s fork.

ohorbatiuk’s picture

Issue summary: View changes
StatusFileSize
new8.66 KB

Updated patch from #6.

nedjo’s picture

Version: 8.x-3.x-dev » 5.0.x-dev
Status: Active » Postponed (maintainer needs more info)

@chmez: thanks for working on this. Can you explain how this patch differs from the previous approach? Will the new approach avoid triggering the circular reference that was the reason we had to revert?

Moving to 5.0.x as no further work will be done on the 8.x branches.

ohorbatiuk’s picture

Hi @nedjo. I can not apply the previous patch on the "8.x-3.x" branch so I've just fixed conflicts, used injected services, deleted unused code. You can see differences between these patches below:

diff --git a/features.services.yml b/features.services.yml
index 1f61849..18274fa 100644
--- a/features.services.yml
+++ b/features.services.yml
@@ -5,11 +5,20 @@ services:
   plugin.manager.features_generation_method:
     class: Drupal\features\FeaturesGenerationMethodManager
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
+
   features_assigner:
     class: Drupal\features\FeaturesAssigner
-    arguments: ['@features.manager', '@plugin.manager.features_assignment_method', '@entity_type.manager', '@config.factory', '@config.storage', '%install_profile%']
+    arguments:
+      - '@features.manager'
+      - '@plugin.manager.features_assignment_method'
+      - '@entity_type.manager'
+      - '@config.factory'
+      - '@config.storage'
+      - '@state'
+      - '%install_profile%'
     calls:
       - [initFeaturesManager]
+
   features_generator:
     class: Drupal\features\FeaturesGenerator
     arguments: ['@features.manager', '@plugin.manager.features_generation_method', '@features_assigner', '@messenger', '@logger.channel.features']
@@ -38,8 +47,8 @@ services:
     decoration_priority: 9
 
   features.config_save_subscriber:
-    class: Drupal\features\EventSubscriber\ConfigSaveSubscriber
-    arguments: ['@entity_type.manager', '@features_assigner']
+    class: Drupal\features\EventSubscriber\FeaturesConfigSaveSubscriber
+    arguments: ['@state']
     tags:
       - { name: event_subscriber }
 
diff --git a/src/EventSubscriber/FeaturesConfigSaveSubscriber.php b/src/EventSubscriber/FeaturesConfigSaveSubscriber.php
new file mode 100644
index 0000000..61e98c5
--- /dev/null
+++ b/src/EventSubscriber/FeaturesConfigSaveSubscriber.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\features\EventSubscriber;
+
+use Drupal\Core\Config\ConfigCrudEvent;
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\State\StateInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Class FeaturesConfigSaveSubscriber.
+ */
+class FeaturesConfigSaveSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The state storage service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a new FeaturesConfigSaveSubscriber object.
+   *
+   * @param \Drupal\Core\State\StateInterface $state
+   *    The state storage service.
+   */
+  public function __construct(StateInterface $state) {
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[ConfigEvents::SAVE] = ['onConfigSave'];
+
+    return $events;
+  }
+
+  /**
+   * Sets a features bundle as current when it is saved.
+   *
+   * @param \Drupal\Core\Config\ConfigCrudEvent $event
+   *   The Event to process.
+   */
+  public function onConfigSave(ConfigCrudEvent $event) {
+    $config = $event->getConfig();
+
+    if (strpos($config->getName(), 'features.bundle.') === 0) {
+      $this->state->set('features.current_bundle',  $config->get('machine_name'));
+    }
+  }
+
+}
diff --git a/src/FeaturesAssigner.php b/src/FeaturesAssigner.php
index 42fde03..e3f55f6 100644
--- a/src/FeaturesAssigner.php
+++ b/src/FeaturesAssigner.php
@@ -7,6 +7,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\ExtensionInstallStorage;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Config\StorageInterface;
+use Drupal\Core\State\StateInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 /**
@@ -71,6 +72,13 @@ class FeaturesAssigner implements FeaturesAssignerInterface {
    */
   protected $currentBundle;
 
+  /**
+   * The state storage service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
   /**
    * The name of the currently active installation profile.
    *
@@ -91,15 +99,26 @@ class FeaturesAssigner implements FeaturesAssignerInterface {
    *   The configuration factory.
    * @param \Drupal\Core\Config\StorageInterface $config_storage
    *   The configuration factory.
+   * @param \Drupal\Core\State\StateInterface $state
+   *    The state storage service.
    * @param string $install_profile
    *   The name of the currently active installation profile.
    */
-  public function __construct(FeaturesManagerInterface $features_manager, PluginManagerInterface $assigner_manager, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, $install_profile) {
+  public function __construct(
+    FeaturesManagerInterface $features_manager,
+    PluginManagerInterface $assigner_manager,
+    EntityTypeManagerInterface $entity_type_manager,
+    ConfigFactoryInterface $config_factory,
+    StorageInterface $config_storage,
+    StateInterface $state,
+    $install_profile
+  ) {
     $this->featuresManager = $features_manager;
     $this->assignerManager = $assigner_manager;
     $this->entityTypeManager = $entity_type_manager;
     $this->configFactory = $config_factory;
     $this->configStorage = $config_storage;
+    $this->state = $state;
     $this->installProfile = $install_profile;
     $this->bundles = $this->getBundleList();
     $this->currentBundle = $this->getBundle(FeaturesBundleInterface::DEFAULT_BUNDLE);
@@ -263,7 +282,9 @@ class FeaturesAssigner implements FeaturesAssignerInterface {
     if (isset($session)) {
       $session->set('features_current_bundle', $bundle->getMachineName());
     }
-    \Drupal::state()->set('features.current_bundle', $bundle->getMachineName());
+
+    $this->state->set('features.current_bundle', $bundle->getMachineName());
+
     return $bundle;
   }
 
@@ -431,18 +452,25 @@ class FeaturesAssigner implements FeaturesAssignerInterface {
   public function loadBundle($machine_name = NULL) {
     if (!isset($machine_name)) {
       $session = \Drupal::request()->getSession();
-      if (isset($session) && ($session_value = $session->get('features_current_bundle'))) {
-        $machine_name = $session_value;
+
+      if (isset($session) && $session->has('features_current_bundle')) {
+        $machine_name = $session->get('features_current_bundle');
       }
       else {
-        $machine_name = \Drupal::state()->get('features.current_bundle',  FeaturesBundleInterface::DEFAULT_BUNDLE) ;
+        $machine_name = $this->state->get(
+          'features.current_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);
   }
 
diff --git a/tests/src/Kernel/EventSubscriber/FeaturesConfigSaveEventSubscriberTest.php b/tests/src/Kernel/EventSubscriber/FeaturesConfigSaveEventSubscriberTest.php
index 4f8dcfe..7e83fd4 100644
--- a/tests/src/Kernel/EventSubscriber/FeaturesConfigSaveEventSubscriberTest.php
+++ b/tests/src/Kernel/EventSubscriber/FeaturesConfigSaveEventSubscriberTest.php
@@ -6,17 +6,17 @@ use Drupal\features\Entity\FeaturesBundle;
 use Drupal\KernelTests\KernelTestBase;
 
 /**
- * @coversDefaultClass \Drupal\features\EventSubscriber\ConfigSaveSubscriber
+ * @coversDefaultClass \Drupal\features\EventSubscriber\FeaturesConfigSaveSubscriber
  * @group features
  */
 class FeaturesConfigSaveEventSubscriberTest extends KernelTestBase {
 
   /**
-   * The features assigner.
+   * The state storage service.
    *
-   * @var \Drupal\features\FeaturesAssignerInterface
+   * @var \Drupal\Core\State\StateInterface
    */
-  protected $assigner;
+  protected $state;
 
   /**
    * {@inheritdoc}
@@ -30,8 +30,7 @@ class FeaturesConfigSaveEventSubscriberTest extends KernelTestBase {
     parent::setUp();
 
     $this->installConfig('features');
-    $this->entityTypeManager = $this->container->get('features_assigner');
-    $this->assigner = $this->container->get('features_assigner');
+    $this->state = $this->container->get('state');
   }
 
   public function testConfigSaveEventSubscriber() {
@@ -39,12 +38,10 @@ class FeaturesConfigSaveEventSubscriberTest extends KernelTestBase {
       'machine_name' => 'test',
       'name' => 'Test',
     ]);
+
     $bundle->save();
 
-    // Get the current bundle.
-    /** @var \Drupal\features\Entity\FeaturesBundleInterface $bundle */
-    $bundle = $this->assigner->getBundle();
-    $this->assertEquals('test', $bundle->getMachineName());
+    $this->assertEquals('test', $this->state->get('features.current_bundle'));
   }
 
 }

Also, I can confirm that my patch can be applied to the "5.0.x" branch.

ohorbatiuk’s picture

Issue summary: View changes
Status: Postponed (maintainer needs more info) » Needs review
StatusFileSize
new8.47 KB

Updated patch from #14.