Config Guardian elevates Drupal configuration management to an enterprise standard. It provides a comprehensive safety net for development teams by introducing point-in-time snapshots, advanced impact analysis, and a reliable rollback engine.

The module follows strict coding standards, implements dependency injection, sanitizes user input in Twig templates, and uses secure JSON serialization for data storage. It has been validated with PHPCS (Drupal Standards) and manual security review.

Project link

https://www.drupal.org/project/config_guardian

AI Disclosure

AI tools were used during the development of this module to assist with boilerplate code generation, test scaffolding, and documentation drafting. All code has been reviewed, tested, and validated by the maintainer.

Comments

andresmoreno28 created an issue. See original summary.

rushikesh raval’s picture

Thank you for applying!

Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.

The important notes are the following.

  • New releases are not necessary for these applications, which could require changes that are not backward-compatible. Not creating new releases avoids any possible issue.
  • Please do not change the branch to review once reviews started, except in the case the used branch needs to be deleted.
  • If you have not done it yet, enable GitLab CI for the project, and fix what reported from the phpcs job. This help to fix most of what reviewers would report.
  • For the time this application is open, only your commits are allowed. No other people, including other maintainers/co-maintainers can make commits.
  • The purpose of this application is giving you a new drupal.org role that allows you to opt projects into security advisory coverage, either projects you already created, or projects you will create. The project status won't be changed by this application.
  • Nobody else will get the permission to opt projects into security advisory policy. If there are other maintainers/co-maintainers who will to get that permission, they need to apply with a different module.
  • We only accept an application per user. If you change your mind about the project to use for this application, or it is necessary to use a different project for the application, please update the issue summary with the link to the correct project and the issue title with the project name and the branch to review.

To the reviewers

Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.

The important notes are the following.

  • It is preferable to wait for a Code Review Administrator before commenting on newly created applications. Code Review Administrators will do some preliminary checks that are necessary before any change on the project files is suggested.
  • Reviewers should show the output of a CLI tool only once per application. The configuration used for these tools needs to be the same configuration used by GitLab CI, stored in the GitLab Templates repository.
  • It may be best to have the applicant fix things before further review.

For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.

rushikesh raval’s picture

Title: [Module] Config Guardian [D10] [D11] » [1.0.x] Config Guardian
Issue summary: View changes
rushikesh raval’s picture

Issue summary: View changes
rushikesh raval’s picture

Issue summary: View changes
rushikesh raval’s picture

Remember to change status, when the project is ready to be reviewed. In this queue, projects are only reviewed when the status is Needs review.

andresmoreno28’s picture

Status: Active » Needs review
rushikesh raval’s picture

Status: Needs review » Needs work

Fix issue reported from the phpcs job

andresmoreno28’s picture

Status: Needs work » Needs review

All CI jobs are now passing and it's ready for review. Thank you so much!

andresmoreno28’s picture

I hope you guys are having a great week.

I'm writing just to gently bump this issue. I updated the module code addressing all the previous feedback. Please let me know if there are any outstanding blockers or if there is anything else I can do to facilitate the review process.

Thanks again for your time and your volunteer work reviewing these applications!

rushikesh raval’s picture

Just go through comment #2. Reviewers will review your code and will update here.

vishal.kadam’s picture

Status: Needs review » Needs work

1. FILE: composer.json

  "require": {
    "drupal/core": "^10.2 || ^11"

As a side note, it is not necessary to add the Drupal core requirements in the /composer.json/ file: The Drupal.org Composer Façade will add them.

2. FILE: config_guardian.module

For a new module that aims to be compatible with Drupal 10 and Drupal 11, I would rather implement hooks as class methods as described in Support for object oriented hook implementations using autowired services.

/**
 * @file
 * Primary module hooks for Config Guardian module.
 */

Drupal does not have primary and secondary hooks. Instead of that, it is preferable to use the usual description: “Hook implementations for the [module name] module”, where [module name] is the name of the module given in its .info.yml file.

3. FILE: src/Commands/ConfigGuardianCommands.php

  /**
   * The snapshot manager service.
   *
   * @var \Drupal\config_guardian\Service\SnapshotManagerService
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The config analyzer service.
   *
   * @var \Drupal\config_guardian\Service\ConfigAnalyzerService
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * The rollback engine service.
   *
   * @var \Drupal\config_guardian\Service\RollbackEngineService
   */
  protected RollbackEngineService $rollbackEngine;

  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ConfigAnalyzerService $config_analyzer,
    RollbackEngineService $rollback_engine,
  ) {
    parent::__construct();
    $this->snapshotManager = $snapshot_manager;
    $this->configAnalyzer = $config_analyzer;
    $this->rollbackEngine = $rollback_engine;
  }

FILE: src/Controller/ActivityController.php

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * Constructs an ActivityController object.
   */
  public function __construct(ActivityLoggerService $activity_logger) {
    $this->activityLogger = $activity_logger;
  }

FILE: src/Controller/AjaxController.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The config analyzer.
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * Constructs an AjaxController object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ConfigAnalyzerService $config_analyzer,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->configAnalyzer = $config_analyzer;
  }

FILE: src/Controller/AnalysisController.php

  /**
   * The config analyzer.
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * Constructs an AnalysisController object.
   */
  public function __construct(ConfigAnalyzerService $config_analyzer) {
    $this->configAnalyzer = $config_analyzer;
  }

FILE: src/Controller/ConfigSyncController.php

  /**
   * The config sync service.
   */
  protected ConfigSyncService $configSync;

  /**
   * Constructs a ConfigSyncController object.
   */
  public function __construct(ConfigSyncService $config_sync) {
    $this->configSync = $config_sync;
  }

FILE: src/Controller/DashboardController.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The config analyzer.
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * Constructs a DashboardController object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ConfigAnalyzerService $config_analyzer,
    ActivityLoggerService $activity_logger,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->configAnalyzer = $config_analyzer;
    $this->activityLogger = $activity_logger;
  }

FILE: src/Controller/DependencyGraphController.php

  /**
   * The config analyzer.
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * Constructs a DependencyGraphController object.
   */
  public function __construct(
    ConfigAnalyzerService $config_analyzer,
  ) {
    $this->configAnalyzer = $config_analyzer;
  }

FILE: src/Controller/SnapshotController.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * Constructs a SnapshotController object.
   */
  public function __construct(SnapshotManagerService $snapshot_manager) {
    $this->snapshotManager = $snapshot_manager;
  }

FILE: src/EventSubscriber/ConfigEventSubscriber.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * The settings service.
   */
  protected SettingsService $settings;

  /**
   * The config factory.
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * Constructs a ConfigEventSubscriber object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ActivityLoggerService $activity_logger,
    SettingsService $settings,
    ConfigFactoryInterface $config_factory,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->activityLogger = $activity_logger;
    $this->settings = $settings;
    $this->configFactory = $config_factory;
  }

FILE: src/Form/ConfigExportForm.php

  /**
   * The config sync service.
   */
  protected ConfigSyncService $configSync;

  /**
   * The snapshot manager service.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The activity logger service.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * Constructs a ConfigExportForm object.
   */
  public function __construct(
    ConfigSyncService $config_sync,
    SnapshotManagerService $snapshot_manager,
    ActivityLoggerService $activity_logger,
  ) {
    $this->configSync = $config_sync;
    $this->snapshotManager = $snapshot_manager;
    $this->activityLogger = $activity_logger;
  }

FILE: src/Form/ConfigImportForm.php

  /**
   * The config sync service.
   */
  protected ConfigSyncService $configSync;

  /**
   * Constructs a ConfigImportForm object.
   */
  public function __construct(ConfigSyncService $config_sync) {
    $this->configSync = $config_sync;
  }

FILE: src/Form/RollbackForm.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The rollback engine.
   */
  protected RollbackEngineService $rollbackEngine;

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * The snapshot data.
   */
  protected ?array $snapshot = NULL;

  /**
   * Constructs a RollbackForm object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    RollbackEngineService $rollback_engine,
    ActivityLoggerService $activity_logger,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->rollbackEngine = $rollback_engine;
    $this->activityLogger = $activity_logger;
  }

FILE: src/Form/SnapshotCreateForm.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * Constructs a SnapshotCreateForm object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ActivityLoggerService $activity_logger,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->activityLogger = $activity_logger;
  }

FILE: src/Form/SnapshotDeleteForm.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * Constructs a SnapshotDeleteForm object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ActivityLoggerService $activity_logger,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->activityLogger = $activity_logger;
  }

FILE: src/Form/SnapshotImportForm.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The activity logger.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * The rollback engine.
   */
  protected RollbackEngineService $rollbackEngine;

  /**
   * Constructs a SnapshotImportForm object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ActivityLoggerService $activity_logger,
    RollbackEngineService $rollback_engine,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->activityLogger = $activity_logger;
    $this->rollbackEngine = $rollback_engine;
  }

FILE: src/Service/ActivityLoggerService.php

  /**
   * The entity type manager.
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The current user.
   */
  protected AccountProxyInterface $currentUser;

  /**
   * The request stack.
   */
  protected RequestStack $requestStack;

  /**
   * The database connection.
   */
  protected Connection $database;

  /**
   * The time service.
   */
  protected TimeInterface $time;

  /**
   * The cache tags invalidator.
   */
  protected CacheTagsInvalidatorInterface $cacheTagsInvalidator;

  /**
   * Constructs an ActivityLoggerService object.
   */
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    AccountProxyInterface $current_user,
    RequestStack $request_stack,
    Connection $database,
    TimeInterface $time,
    CacheTagsInvalidatorInterface $cache_tags_invalidator,
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->currentUser = $current_user;
    $this->requestStack = $request_stack;
    $this->database = $database;
    $this->time = $time;
    $this->cacheTagsInvalidator = $cache_tags_invalidator;
  }

FILE: src/Service/ConfigAnalyzerService.php

  /**
   * The config factory.
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The active config storage.
   */
  protected StorageInterface $activeStorage;

  /**
   * The sync config storage.
   */
  protected StorageInterface $syncStorage;

  /**
   * The module handler.
   */
  protected ModuleHandlerInterface $moduleHandler;

  /**
   * The config manager.
   */
  protected ConfigManagerInterface $configManager;

  /**
   * Constructs a ConfigAnalyzerService object.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    StorageInterface $active_storage,
    StorageInterface $sync_storage,
    ModuleHandlerInterface $module_handler,
    ConfigManagerInterface $config_manager,
  ) {
    $this->configFactory = $config_factory;
    $this->activeStorage = $active_storage;
    $this->syncStorage = $sync_storage;
    $this->moduleHandler = $module_handler;
    $this->configManager = $config_manager;
  }

FILE: src/Service/ConfigSyncService.php

  /**
   * The active config storage.
   */
  protected StorageInterface $activeStorage;

  /**
   * The sync config storage.
   */
  protected StorageInterface $syncStorage;

  /**
   * The config manager.
   */
  protected ConfigManagerInterface $configManager;

  /**
   * The typed config manager.
   */
  protected TypedConfigManagerInterface $typedConfigManager;

  /**
   * The lock backend.
   */
  protected LockBackendInterface $lock;

  /**
   * The event dispatcher.
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The module handler.
   */
  protected ModuleHandlerInterface $moduleHandler;

  /**
   * The module installer.
   */
  protected ModuleInstallerInterface $moduleInstaller;

  /**
   * The theme handler.
   */
  protected ThemeHandlerInterface $themeHandler;

  /**
   * The module extension list.
   */
  protected ModuleExtensionList $moduleExtensionList;

  /**
   * The theme extension list.
   */
  protected ThemeExtensionList $themeExtensionList;

  /**
   * The config analyzer service.
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * The activity logger service.
   */
  protected ActivityLoggerService $activityLogger;

  /**
   * The snapshot manager service.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * Constructs a ConfigSyncService object.
   */
  public function __construct(
    StorageInterface $active_storage,
    StorageInterface $sync_storage,
    ConfigManagerInterface $config_manager,
    TypedConfigManagerInterface $typed_config_manager,
    LockBackendInterface $lock,
    EventDispatcherInterface $event_dispatcher,
    ModuleHandlerInterface $module_handler,
    ModuleInstallerInterface $module_installer,
    ThemeHandlerInterface $theme_handler,
    ModuleExtensionList $module_extension_list,
    ThemeExtensionList $theme_extension_list,
    ConfigAnalyzerService $config_analyzer,
    ActivityLoggerService $activity_logger,
    SnapshotManagerService $snapshot_manager,
  ) {
    $this->activeStorage = $active_storage;
    $this->syncStorage = $sync_storage;
    $this->configManager = $config_manager;
    $this->typedConfigManager = $typed_config_manager;
    $this->lock = $lock;
    $this->eventDispatcher = $event_dispatcher;
    $this->moduleHandler = $module_handler;
    $this->moduleInstaller = $module_installer;
    $this->themeHandler = $theme_handler;
    $this->moduleExtensionList = $module_extension_list;
    $this->themeExtensionList = $theme_extension_list;
    $this->configAnalyzer = $config_analyzer;
    $this->activityLogger = $activity_logger;
    $this->snapshotManager = $snapshot_manager;
  }

FILE: src/Service/RollbackEngineService.php

  /**
   * The snapshot manager.
   */
  protected SnapshotManagerService $snapshotManager;

  /**
   * The config analyzer.
   */
  protected ConfigAnalyzerService $configAnalyzer;

  /**
   * The settings service.
   */
  protected SettingsService $settings;

  /**
   * The config storage (active).
   */
  protected StorageInterface $configStorage;

  /**
   * The sync config storage.
   */
  protected StorageInterface $syncStorage;

  /**
   * The config factory.
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The config manager.
   */
  protected ConfigManagerInterface $configManager;

  /**
   * The event dispatcher.
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The lock backend.
   */
  protected LockBackendInterface $lock;

  /**
   * The typed config manager.
   */
  protected TypedConfigManagerInterface $typedConfigManager;

  /**
   * The module handler.
   */
  protected ModuleHandlerInterface $moduleHandler;

  /**
   * The module installer.
   */
  protected ModuleInstallerInterface $moduleInstaller;

  /**
   * The theme handler.
   */
  protected ThemeHandlerInterface $themeHandler;

  /**
   * The string translation.
   */
  protected TranslationInterface $stringTranslation;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * Constructs a RollbackEngineService object.
   */
  public function __construct(
    SnapshotManagerService $snapshot_manager,
    ConfigAnalyzerService $config_analyzer,
    StorageInterface $config_storage,
    StorageInterface $sync_storage,
    ConfigFactoryInterface $config_factory,
    ConfigManagerInterface $config_manager,
    EventDispatcherInterface $event_dispatcher,
    LockBackendInterface $lock,
    TypedConfigManagerInterface $typed_config_manager,
    ModuleHandlerInterface $module_handler,
    ModuleInstallerInterface $module_installer,
    ThemeHandlerInterface $theme_handler,
    TranslationInterface $string_translation,
    LoggerChannelFactoryInterface $logger_factory,
    SettingsService $settings,
  ) {
    $this->snapshotManager = $snapshot_manager;
    $this->configAnalyzer = $config_analyzer;
    $this->configStorage = $config_storage;
    $this->syncStorage = $sync_storage;
    $this->configFactory = $config_factory;
    $this->configManager = $config_manager;
    $this->eventDispatcher = $event_dispatcher;
    $this->lock = $lock;
    $this->typedConfigManager = $typed_config_manager;
    $this->moduleHandler = $module_handler;
    $this->moduleInstaller = $module_installer;
    $this->themeHandler = $theme_handler;
    $this->stringTranslation = $string_translation;
    $this->logger = $logger_factory->get('config_guardian');
    $this->settings = $settings;
  }

FILE: src/Service/SettingsService.php

  /**
   * The config factory.
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * Constructs a SettingsService object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
  }

FILE: src/Service/SnapshotManagerService.php

  /**
   * The entity type manager.
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The active config storage.
   */
  protected StorageInterface $configStorage;

  /**
   * The sync config storage.
   */
  protected StorageInterface $syncStorage;

  /**
   * The config factory.
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The event dispatcher.
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The current user.
   */
  protected AccountProxyInterface $currentUser;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * The settings service.
   */
  protected SettingsService $settings;

  /**
   * The database connection.
   */
  protected Connection $database;

  /**
   * The UUID generator.
   */
  protected UuidInterface $uuid;

  /**
   * The time service.
   */
  protected TimeInterface $time;

  /**
   * The cache tags invalidator.
   */
  protected CacheTagsInvalidatorInterface $cacheTagsInvalidator;

  /**
   * Constructs a SnapshotManagerService object.
   */
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    StorageInterface $config_storage,
    StorageInterface $sync_storage,
    ConfigFactoryInterface $config_factory,
    EventDispatcherInterface $event_dispatcher,
    AccountProxyInterface $current_user,
    LoggerChannelFactoryInterface $logger_factory,
    SettingsService $settings,
    Connection $database,
    UuidInterface $uuid,
    TimeInterface $time,
    CacheTagsInvalidatorInterface $cache_tags_invalidator,
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->configStorage = $config_storage;
    $this->syncStorage = $sync_storage;
    $this->configFactory = $config_factory;
    $this->eventDispatcher = $event_dispatcher;
    $this->currentUser = $current_user;
    $this->logger = $logger_factory->get('config_guardian');
    $this->settings = $settings;
    $this->database = $database;
    $this->uuid = $uuid;
    $this->time = $time;
    $this->cacheTagsInvalidator = $cache_tags_invalidator;
  }

New modules, which are compatible with Drupal 10 and higher versions are expected to include type declarations in property definitions, and use constructor property promotion.

4. FILE: src/Controller/ActivityController.php, src/Controller/DashboardController.php

Since that class does not use methods from the parent class, it does not need to use ControllerBase as parent class. Controllers do not need to have a parent class; as long as they implement \Drupal\Core\DependencyInjection\ContainerInjectionInterface, they are fine.

5. FILE: src/Form/SettingsForm.php

With Drupal 10 and Drupal 11, there is no longer need to use #default_value for each form element, when the parent class is ConfigFormBase: It is sufficient to use #config_target, as in the following code.

    $form['image_toolkit'] = [
      '#type' => 'radios',
      '#title' => $this->t('Select an image processing toolkit'),
      '#config_target' => 'system.image:toolkit',
      '#options' => [],
    ];

Using that code, it is no longer needed to save the configuration values in the form submission handler: The parent class will take care of that.
For this change, it is necessary to require at least Drupal 10.3, but that is not an issue, since Drupal 10.2.x is no longer supported.

vishal.kadam’s picture

Issue summary: View changes
andresmoreno28’s picture

Status: Needs work » Needs review

Thank you for the detailed feedback. I have addressed all the points raised:

1. composer.json: Removed `drupal/core` from require section, keeping only `php: >=8.1`.

2. OOP Hooks: Migrated all hooks to a new `ConfigGuardianHooks` class using `#[Hook()]` attributes as described in [Support for object oriented hook implementations](https://www.drupal.org/node/3442349). The class is registered as a service with the `drupal.hook` tag.

3. Constructor Property Promotion: Applied PHP 8.1+ constructor property promotion to all 22 files with dependency injection (services, controllers, forms, commands, and event subscriber).

4. ContainerInjectionInterface: Changed `ActivityController` and `DashboardController` from `extends ControllerBase` to `implements ContainerInjectionInterface` since they don't use any methods from the parent class.

5. #config_target: Updated `SettingsForm` to use `#config_target` for all simple form elements. The `exclude_patterns` field still requires manual handling due to the array-to-textarea conversion.

6. Module file description: Updated to "Hook implementations for the Config Guardian module."

All changes have been tested and the module is working correctly. Ready for re-review.

vishal.kadam’s picture

Issue summary: View changes
Status: Needs review » Needs work

FILE: config_guardian.module

Since the module is declared compatible with Drupal 10.2, removing the function implementing the hook is not possible. The function still needs to be defined, but it calls the method defined by the service class, as described in Support for object oriented hook implementations using autowired services (Backwards-compatible Hook implementation for Drupal versions from 10.1 to 11.0).

andresmoreno28’s picture

Status: Needs work » Needs review

Thank you for the feedback again. I have updated the implementation to maintain backwards compatibility with Drupal 10.2:

The `config_guardian.module` file now contains hook functions that delegate to the OOP service class methods.

This ensures the module works on:
- **Drupal 10.2**: Uses the procedural hooks in `.module`
- **Drupal 10.3+/11**: Uses the `#[Hook()]` attributes in the service class

Ready for re-review.

vishal.kadam’s picture

Rest seems fine to me.

Please wait for other reviewers and Project Moderator to take a look and if everything goes fine, you will get the role.

andresmoreno28’s picture

Thank you so much, vishal.kadam! I'll wait for other reviewers and PMs. :)

andresmoreno28’s picture

I am writing to follow up on this application. It has been about a month since the last community review, which appeared to be positive, so I wanted to touch base to see if there is anything else required on my end.

I understand the queue can be busy, but I wanted to make sure the application hasn't slipped through the cracks. I am eager to move forward with the security advisory coverage opt-in.

Thank you very much for your time and help!!

rushikesh raval’s picture

Priority: Normal » Major

Chaning the priority as per Issue Priorities.

nkmani’s picture

Hi andresmoreno28,

I briefly reviewed your code ... my comments are purely based on what would be expected in terms of testing;

- The README has instructions for running tests and you claim that tests are included for new features. But I don't see any tests folder.
- There are modules like config_ignore, config_split to name a few, that modify the configuration. I would be curious to know how your module works along side others.

Thanks!

avpaderno’s picture

Priority: Major » Normal
Status: Needs review » Needs work
andresmoreno28’s picture

Status: Needs work » Needs review

Hi, thanks for taking the time to review the module and for the feedback.

You're absolutely right about the tests — that was an oversight on my part. I had them locally but they never made it into the repository. I've now pushed the full test suite: 132 tests (73 unit + 59 kernel) covering all services, models, event subscribers, and the installation schema. They all pass on the CI pipeline.

I also took the opportunity to fix a couple of minor bugs the tests uncovered in the filtered count queries, and corrected some inaccuracies in the README.

Regarding compatibility with other configuration modules: I've added a dedicated section to the README explaining how Config Guardian works alongside config_split, config_ignore, and config_readonly. In short, Config Guardian operates at the storage level — it captures raw snapshots of both the active and sync configuration directories without interfering with how other modules filter, split, or lock configuration during import/export. It essentially provides a safety net (snapshots + rollback) that complements whatever configuration workflow you have in place.

Let me know if there's anything else I should address. Thanks again for the review!

batigolix’s picture

Status: Needs review » Needs work

If the module code includes any AI generated content, then it is a good practices to disclose this (on the project page or in the application). See https://www.drupal.org/docs/develop/issues/issue-procedures-and-etiquett...

andresmoreno28’s picture

Issue summary: View changes
andresmoreno28’s picture

Status: Needs work » Needs review

Hi, batigolix. You're right — AI tools were used during development to assist with scaffolding, boilerplate, and documentation drafts. I've added an AI Disclosure section to this application as recommended. Everything has been reviewed and tested, but I appreciate you pointing to the guidelines. Let me know if there's anything else that needs attention.

batigolix’s picture

Status: Needs review » Needs work

Thanks.

I was not clear in my previous comment. With "application" I meant the module, or the code. Not this security coverage application.

So the guideline asks to disclose AI usage. You could do that on the project page, or in the readme. Here is an example of how to include it in the project page: https://www.drupal.org/project/mcp_server

andresmoreno28’s picture

Status: Needs work » Needs review

Thanks for clarifying.

I've added an AI Disclosure section to the README acknowledging that AI tools were used to assist with boilerplate generation, test scaffolding, and documentation drafting. All code has been reviewed, tested, and validated.

batigolix’s picture

Looks great!

andresmoreno28’s picture

Thanks for the review! If there's anything else I should address, please let me know. :)

andresmoreno28’s picture

Priority: Normal » Major

Since it's been more than 3 weeks since the last response, I'm changing the priority as stipulated in Issue priorities.

andresmoreno28’s picture

Priority: Major » Critical

Since it's been more than 8 weeks since the last response, I'm changing the priority as stipulated in Issue priorities.

Thank you!

avpaderno’s picture

Priority: Critical » Normal
Status: Needs review » Needs work
  • The following points are just a start and don't necessarily encompass all of the changes that may be necessary
  • A specific point may just be an example and may apply in other places
  • A review is about code that doesn't follow the coding standards, contains possible security issue, or does not correctly use the Drupal API; the single points are not ordered, not even by importance

src/Controller/AjaxController.php

Since that class does not use methods from the parent class, or it uses a single method from the parent class, it does not need to use ControllerBase as parent class.

Controllers do not need to have a parent class; as long as they implement \Drupal\Core\DependencyInjection\ContainerInjectionInterface, they are fine.

src/Form/ConfigExportForm.php

        $this->t('@count total configurations', ['@count' => $preview['total']]),

For translatable strings that change basing on a number (as in this case), there is ::formatPlural().

src/Form/ConfigImportForm.php

      foreach ($this->preview['conflicts'] as $conflict) {
        $conflict_items[] = [
          '#markup' => '<strong>' . $conflict['config'] . '</strong>: ' . $conflict['details'] .
          ' <span class="cg-conflict-severity cg-conflict-severity--' . $conflict['severity'] . '">(' . $conflict['severity'] . ')</span>',
        ];
      }

For outputting HTML markup, it is probably better to use template files, or a rendering class.

src/Model/RiskAssessment.php

  public function getDescription(): string {
    $descriptions = [
      'low' => 'Low risk - Changes are safe to apply.',
      'medium' => 'Medium risk - Review changes before applying.',
      'high' => 'High risk - Carefully review all changes.',
      'critical' => 'Critical risk - Consider creating a backup first.',
    ];

    return $descriptions[$this->level] ?? '';
  }

If those descriptions are visible in the user interface, they should be translatable string.

src/Service/ConfigAnalyzerService.php

    if ($import['total'] === 0 && $export['total'] === 0) {
      $status = 'synced';
      $recommendation = 'none';
    }
    elseif ($sync_count === 0 && $active_count > 0) {
      // Sync directory is empty but active has configs.
      $status = 'needs_export';
      $recommendation = 'export';
      $warnings[] = [
        'type' => 'info',
        'message' => 'El directorio sync está vacío. Exporta la configuración activa para inicializarlo.',
      ];
    }
    elseif ($active_count === 0 && $sync_count > 0) {
      // Active is empty but sync has configs (fresh install).
      $status = 'needs_import';
      $recommendation = 'import';
      $warnings[] = [
        'type' => 'info',
        'message' => 'El sitio no tiene configuración activa. Importa desde sync para configurar el sitio.',
      ];
    }
    elseif (count($only_in_active) > 0 && count($only_in_sync) === 0 && empty($modified)) {
      // Only active has extra configs, sync is subset.
      $status = 'needs_export';
      $recommendation = 'export';
      $warnings[] = [
        'type' => 'info',
        'message' => 'Hay ' . count($only_in_active) . ' configuraciones nuevas en active que no están en sync.',
      ];
    }

If those strings are visible in the user interface, they should be translatable strings, for which the English version is provided.

src/Service/RollbackEngineService.php

      if ($simulation->totalChanges > 50) {
        $risk->riskFactors[] = "Large number of sync changes ({$simulation->totalChanges} files)";
      }
      if (count($to_delete) > 10) {
        $risk->riskFactors[] = count($to_delete) . " files will be removed from sync";
      }

Those strings should be translatable strings. As such, they should use placeholders, instead of string concatenation.

andresmoreno28’s picture

Status: Needs work » Needs review

Hi,

Thanks for the review. I've pushed the fixes on 1.0.x (commit a015834), applying each point across the rest of the module too. PHPCS clean and unit tests passing.

Could you take another look when you have a moment? Thanks!

andresmoreno28’s picture

Good afternoon! Is there anything else that needs to be fixed, or is it all set? Sorry for the insistence, and thank you very much!

vishal.kadam’s picture

Priority: Normal » Major

I am changing priority as per Issue priorities.

avpaderno’s picture

Priority: Major » Normal
Status: Needs review » Needs work

src/Controller/AjaxController.php

        $message = $this->formatPlural(
          $total,
          '1 pending change.',
          '@count pending changes.'
        );

the @count placeholder needs to be used also for the singular form, since in some languages the singular form is used also for numbers which in English are plural. For example, in Russian it is 101 яблоко and 1 яблоко, where яблоко means apple.

src/Controller/AnalysisController.php

With Drupal 10.3 and higher versions, a controller class does not need to implement create(); using AutowireTrait, it just needs to implement the class constructor.
The core requirements need to be changed, but that is not an issue, since Drupal 10.3 is no longer supported. Given the recent releases that fixed security issues, it is probably better to require at least Drupal 10.5.x or Drupal 10.6.x.

src/Controller/DependencyGraphController.php

Since that class does not use methods from the parent class, or it uses a single method from the parent class, it does not need to use ControllerBase as parent class.
Controllers do not need to have a parent class; as long as they implement \Drupal\Core\DependencyInjection\ContainerInjectionInterface, they are fine.

$html = <<<HTML
// Omitted
HTML;

For outputting HTML markup, it is probably better to use template files, or a rendering class, especially when the markup is complex.

src/EventSubscriber/ConfigEventSubscriber.php

      catch (\Exception $e) {
        // Log but don't prevent import.
        \Drupal::logger('config_guardian')->warning('Failed to create pre-import snapshot: @message', [
          '@message' => $e->getMessage(),
        ]);
      }

In the case of exceptions, it is probably also important to log the backtrace, which allows to better understand what caused the exception. For that, Drupal has Error::logException().

andresmoreno28’s picture

Thanks for the thorough review, @avpaderno. I've addressed every point, and since each one can apply in other places, I went through the whole module rather than only the cited lines.

formatPlural() singular form

Every formatPlural() / PluralTranslatableMarkup call now uses @count in the singular form as well — across controllers, forms, services, batch operations and the Twig templates:

$message = $this->formatPlural(
  $total,
  '@count pending change.',
  '@count pending changes.'
);

Controllers use AutowireTrait instead of create()

No controller implements create() anymore; they implement ContainerInjectionInterface and use AutowireTrait, with custom services wired through #[Autowire]. core_version_requirement is now ^10.5 || ^11 (and the system dependency follows).

class AnalysisController implements ContainerInjectionInterface {

  use AutowireTrait;
  use StringTranslationTrait;

  public function __construct(
    #[Autowire(service: 'config_guardian.config_analyzer')]
    protected readonly ConfigAnalyzerService $configAnalyzer,
  ) {}

}

No ControllerBase

No controller extends ControllerBase now. DependencyGraphController injects the ModuleHandlerInterface, LanguageManagerInterface and RendererInterface it actually uses.

HTML markup

The dependency-graph page is no longer built from a heredoc; it is rendered from a dedicated Twig template (config-guardian-dependency-graph.html.twig). Other inline markup in the forms was moved to templates, html_tag render elements, or render arrays.

Exceptions logged with Error::logException()

Every catch block that logs an exception now records the backtrace, and ConfigEventSubscriber / ConfigSyncService were given an injected logger instead of \Drupal::logger():

catch (\Exception $e) {
  Error::logException($this->logger, $e, 'Failed to create pre-import snapshot: @message', [], LogLevel::WARNING);
}

While going through the code I also removed three JavaScript files that were not loaded by any library (one depended on an undeclared third-party d3 library) and switched the snapshot details output to #plain_text.

Everything is on the 1.0.x branch (latest commit fd9395c). PHP_CodeSniffer (Drupal, DrupalPractice) is clean and the unit tests pass. Back to "Needs review" — thanks again for your time.

andresmoreno28’s picture

Status: Needs work » Needs review
avpaderno’s picture

Status: Needs review » Needs work

config_guardian.info.yml

dependencies:
  - drupal:config
  - drupal:system (>=10.5)
  - drupal:user

Requiring the System module is not necessary, since that module is always installed in any Drupal site. That line was used to require a specific Drupal core version, but that is no longer necessary now that Drupal core uses core_version_requirement.

config_guardian.services.yml

Services can be autowired starting with Drupal 9.3.x. This means that it is no longer necessary to give a list of service arguments in the .services.yml file; they will be retrieved from the constructor definition.

src/Hook/ConfigGuardianHooks.php

It is preferable to initialize the string translation service by calling ::setStringTranslation().
StringTranslationTrait calls \Drupal::service('string_translation') for backward-compatibility reasons, but the service object should be explicitly initialized.

It is preferable to store the logger factory in a class property. Storing a logger instance is considered an anti-pattern by some developers.

src/Controller/DashboardController.php

  public function __construct(
    #[Autowire(service: 'config_guardian.snapshot_manager')]
    protected readonly SnapshotManagerService $snapshotManager,
    #[Autowire(service: 'config_guardian.config_analyzer')]
    protected readonly ConfigAnalyzerService $configAnalyzer,
    #[Autowire(service: 'config_guardian.activity_logger')]
    protected readonly ActivityLoggerService $activityLogger,
  ) {}

Using the class namespaced-name as service ID, it is not necessary to use #[Autowire()] for those parameters. The new service ID can be added as alias.
I would also suggest creating another alias using the identifier of the interface implemented by the service class.

src/Form/ConfigExportForm.php

          '#title' => $this->formatPlural(
            count($preview['new']),
            'New Configurations (@count)',
            'New Configurations (@count)'
          ),

It is better to use @count new configuration for the singular, if ::formatPlural() is used. Otherwise, it is not necessary to use ::formatPlural().

src/Form/RollbackForm.php

        $form['simulation']['summary'] = [
          '#type' => 'html_tag',
          '#tag' => 'p',
          '#value' => $this->t('This restore will make <strong>@total</strong> total changes:', ['@total' => $simulation->totalChanges]),
        ];

This is instead a case where ::formatPlural() should be used. I would also just use <strong>@total</strong> change / <strong>@total</strong> changes.

andresmoreno28’s picture

Status: Needs work » Needs review

Thanks again, avpaderno. All six points are addressed, applied across the whole module:

config_guardian.info.yml — Removed the system dependency (and user; both are always installed). Added drupal:file, which provides file_save_upload() used by the snapshot import form.

config_guardian.services.yml — Switched to autowiring: each service is registered by its class name with autowire: true and a short-name alias; the argument lists are gone. #[Autowire(service: '…')] remains only on the genuinely ambiguous parameters (the two config storages, lock, event_dispatcher). I also removed several unused injected dependencies found along the way.

ConfigGuardianHooks — String translation is now initialised explicitly via setStringTranslation() (through the service definition), and the logger factory is stored in a property instead of a logger instance. Same treatment for the other classes using the trait.

DashboardController — Since services are registered under their class name (with aliases), the #[Autowire] attributes are no longer needed; removed from all controllers.

class AnalysisController implements ContainerInjectionInterface {

  use AutowireTrait;
  use StringTranslationTrait;

  public function __construct(
    protected readonly ConfigAnalyzerService $configAnalyzer,
    TranslationInterface $string_translation,
  ) {
    $this->setStringTranslation($string_translation);
  }

}

ConfigExportForm / formatPlural() — The singular now uses @count, and singular/plural are never identical: where a noun is counted I use real forms (@count new configuration / @count new configurations), and where the label does not change with the count I use plain t() with an @count placeholder. Applied to every formatPlural() call in the module.

RollbackForm — The total-changes line now uses ::formatPlural() (@count change / @count changes).

While going through the code I also removed routing parameter converters for a non-existent entity type, generated all URLs from routes instead of hard-coding paths, mapped activity action names to translatable labels in PHP instead of passing dynamic values to the Twig |t filter, and replaced fetchAll(\PDO::FETCH_ASSOC) (deprecated in 11.2) with a version-neutral conversion.

PHP_CodeSniffer (Drupal, DrupalPractice) is clean and the unit and kernel tests pass on the 1.0.x branch (latest commit b85ffc5). Back to "Needs review" — thanks for your patience.

avpaderno’s picture

Assigned: Unassigned » avpaderno
Status: Needs review » Reviewed & tested by the community

Thank you for your contribution and for your patience with the review process!

I am going to update your account so you can opt into security advisory coverage any project you create, including the projects you already created.

These are some recommended readings to help you with maintainership:

You can find more contributors chatting on Slack or IRC in #drupal-contribute. So, come hang out and stay involved!
Anyone is welcome to participate in the review process. Please consider reviewing other projects that are pending review. I encourage you to learn more about that process and join the group of reviewers.

I thank also all the reviewers for helping with these applications.

avpaderno’s picture

Status: Reviewed & tested by the community » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

andresmoreno28’s picture

Thank you all very much. I really appreciate the time and the thorough, patient
feedback throughout the review.

Special thanks to @avpaderno for the detailed, line-by-line reviews across several
rounds, and to @rushikesh raval, @vishal.kadam, @nkmani and @batigolix for taking
the time to look at the project and raise useful points (tests, module
interactions, coding standards and API usage). Every comment made Config Guardian
noticeably better.

All the feedback has been applied across the whole module, and the improvements
are now released in 1.0.1. Thanks again to the reviewers and to everyone keeping
the application queue going.

Andrés M.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.