diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..96c12d6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# This file is copied from config/drupal-module/.editorconfig in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = LF +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,css,scss}] +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + +[config/sync/**/*.{yml,yaml}] +indent_size = 2 + +[*.{php,install,module,theme}] +indent_size = 2 diff --git a/.env b/.env new file mode 100644 index 0000000..928dbf6 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +COMPOSE_PROJECT_NAME=drupal_config_helper +COMPOSE_DOMAIN=drupal_config_helper.local.itkdev.dk +ITKDEV_TEMPLATE=drupal-module diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..483da6e --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,29 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/changelog.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Changelog +### +### Checks that changelog has been updated + +name: Changelog + +on: + pull_request: + +jobs: + changelog: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Git fetch + run: git fetch + + - name: Check that changelog has been updated. + run: git diff --exit-code origin/${{ github.base_ref }} -- CHANGELOG.md && exit 1 || exit 0 diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml new file mode 100644 index 0000000..6c3a30c --- /dev/null +++ b/.github/workflows/composer.yaml @@ -0,0 +1,80 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/composer.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Composer +### +### Validates composer.json and checks that it's normalized. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [ergebnis/composer-normalize](https://github.com/ergebnis/composer-normalize) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev ergebnis/composer-normalize +### ``` +### +### Normalize `composer.json` by running +### +### ``` shell +### docker compose run --rm phpfpm composer normalize +### ``` + +name: Composer + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + composer-validate: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer validate --strict + + composer-normalized: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm composer normalize --dry-run + + composer-audit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer audit diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml new file mode 100644 index 0000000..f8bcf09 --- /dev/null +++ b/.github/workflows/markdown.yaml @@ -0,0 +1,44 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/markdown.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Markdown +### +### Lints Markdown files (`**/*.md`) in the project. +### +### [markdownlint-cli configuration +### files](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration), +### `.markdownlint.jsonc` and `.markdownlintignore`, control what is actually +### linted and how. +### +### #### Assumptions +### +### 1. A docker compose service named `markdownlint` for running `markdownlint` +### (from +### [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli)) +### exists. + +name: Markdown + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + markdown-lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm markdownlint markdownlint '**/*.md' diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000..95c22ed --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,59 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/drupal-module/php.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Drupal module PHP +### +### Checks that PHP code adheres to the [Drupal coding +### standards](https://www.drupal.org/docs/develop/standards). +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [drupal/coder](https://www.drupal.org/project/coder) is a dev requirement +### in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev drupal/coder +### ``` +### +### Clean up and check code by running +### +### ``` shell +### docker compose run --rm phpfpm vendor/bin/phpcbf +### docker compose run --rm phpfpm vendor/bin/phpcs +### ``` +### +### > [!NOTE] +### > The template adds `.phpcs.xml.dist` as [a configuration file for +### > PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file) +### > and this makes it possible to override the actual configuration used in a +### > project by adding a more important configuration file, e.g. `.phpcs.xml`. + +name: PHP + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + coding-standards: + name: PHP - Check Coding Standards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm vendor/bin/phpcs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..fa306ac --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,31 @@ +on: pull_request +name: Review +jobs: + php-code-analysis: + name: PHP code analysis + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ["8.3"] + steps: + - uses: actions/checkout@master + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: json + coverage: none + tools: composer:v2 + # https://github.com/shivammathur/setup-php#cache-composer-dependencies + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Code analysis + run: | + ./scripts/code-analysis diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml new file mode 100644 index 0000000..8c60963 --- /dev/null +++ b/.github/workflows/yaml.yaml @@ -0,0 +1,41 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/yaml.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### YAML +### +### Validates YAML files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. +### +### #### Symfony YAML +### +### Symfony's YAML config files use 4 spaces for indentation and single quotes. +### Therefore we use a [Prettier configuration +### file](https://prettier.io/docs/configuration), `.prettierrc.yaml`, to make +### Prettier format YAML files in the `config/` folder like Symfony expects. + +name: YAML + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier '**/*.{yml,yaml}' --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..987e2a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..0253096 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,22 @@ +// This file is copied from config/markdown/.markdownlint.jsonc in https://github.com/itk-dev/devops_itkdev-docker. +// Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +// markdownlint-cli configuration file (cf. https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration) +{ + "default": true, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + "line-length": { + "line_length": 120, + "code_blocks": false, + "tables": false + }, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md + "no-duplicate-heading": { + "siblings_only": true + }, + // https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections#creating-a-collapsed-section + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md + "no-inline-html": { + "allowed_elements": ["details", "summary"] + } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..d143ace --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,12 @@ +# This file is copied from config/markdown/.markdownlintignore in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#ignoring-files +vendor/ +node_modules/ +LICENSE.md +# Drupal +web/*.md +web/core/ +web/libraries/ +web/*/contrib/ diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..a97dd83 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,31 @@ + + + + + + The coding standard. + + . + + + node_modules + vendor + *.css + *.js + + + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f866789 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/rimi-itk/drupal_config_helper diff --git a/README.md b/README.md index af6f922..ecbcf17 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ # Config helper + +Adds some useful commands for managing config: + +```shell +config:rename Rename config. +config:add-module-dependencies Enforce module dependencies in config. +config:move-module-config Move config info config/install folder in a module. +``` + +**Note**: The `config:move-module-config` command is somewhat is similar to the +[`config:export:content:type`](https://drupalconsole.com/docs/en/commands/config-export-content-type) command the +[Drupal Console](https://drupalconsole.com/), but that command does not add the dependencies needed for our +requirements. + +## Coding standards + +Our coding are checked by GitHub Actions (cf. +[.github/workflows/pr.yml](.github/workflows/pr.yml)). Use the commands below to +run the checks locally. + +### PHP + +```sh +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm composer install +# Fix (some) coding standards issues +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm composer coding-standards-apply +# Check that code adheres to the coding standards +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm composer coding-standards-check +``` + +### Markdown + +```sh +docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' --fix +docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' +``` + +## Code analysis + +We use [PHPStan](https://phpstan.org/) for static code analysis. + +Running statis code analysis on a standalone Drupal module is a bit tricky, so we use a helper script to run the +analysis: + +```sh +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm ./scripts/code-analysis +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b30d95d --- /dev/null +++ b/composer.json @@ -0,0 +1,57 @@ +{ + "name": "itk-dev/drupal_config_helper", + "description": "Config helper", + "license": "GPL-2.0-or-later", + "type": "drupal-module", + "keywords": [ + "Drupal" + ], + "require": { + "php": "^8.3", + "drush/drush": "^12.5" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "drupal/coder": "^8.3", + "drupal/maillog": "^1.1", + "ergebnis/composer-normalize": "^2.47", + "mglaman/phpstan-drupal": "^1.2", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-deprecation-rules": "^1.1" + }, + "repositories": { + "drupal": { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + }, + "minimum-stability": "stable", + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true + }, + "sort-packages": true + }, + "scripts": { + "code-analysis": [ + "@code-analysis/phpstan" + ], + "code-analysis/phpstan": [ + "phpstan analyse" + ], + "coding-standards-apply": [ + "@coding-standards-apply/phpcs" + ], + "coding-standards-apply/phpcs": [ + "phpcbf --standard=phpcs.xml.dist" + ], + "coding-standards-check": [ + "@coding-standards-check/phpcs" + ], + "coding-standards-check/phpcs": [ + "phpcs --standard=phpcs.xml.dist" + ] + } +} diff --git a/config_helper.info.yml b/config_helper.info.yml new file mode 100644 index 0000000..ac3b17c --- /dev/null +++ b/config_helper.info.yml @@ -0,0 +1,8 @@ +name: "config_helper" +type: module +description: "Config helper" +# Used only for development and testing. +hidden: true +core_version_requirement: ^10 +package: "ITK" +# https://www.drupal.org/node/2087879 diff --git a/config_helper.services.yml b/config_helper.services.yml new file mode 100644 index 0000000..08aaf35 --- /dev/null +++ b/config_helper.services.yml @@ -0,0 +1,6 @@ +services: + Drupal\config_helper\ConfigHelper: + arguments: + - "@module_handler" + - "@config.factory" + - "@file_system" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f5d788a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +# itk-version: 3.2.4 + +services: + phpfpm: + image: itkdev/php8.3-fpm:latest + user: ${COMPOSE_USER:-deploy} + volumes: + - .:/app + + # Code checks tools + markdownlint: + image: itkdev/markdownlint + profiles: + - dev + volumes: + - ./:/md + + prettier: + # Prettier does not (yet, fcf. + # https://github.com/prettier/prettier/issues/15206) have an official + # docker image. + # https://hub.docker.com/r/jauderho/prettier is good candidate (cf. https://hub.docker.com/search?q=prettier&sort=updated_at&order=desc) + image: jauderho/prettier + profiles: + - dev + volumes: + - ./:/work diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..de59b15 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,24 @@ + + + PHP Code Sniffer configuration + + . + vendor/ + node_modules/ + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..38f71c1 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +parameters: + level: 6 + paths: + - src + + ignoreErrors: + - "#Method [a-zA-Z0-9\\_\\\\:\\(\\)]+ has parameter \\$[a-zA-Z0-9_]+ with no value type specified in iterable type array#" + - "#Method [a-zA-Z0-9\\_\\\\:\\(\\)]+ return type has no value type specified in iterable type array#" + - "#Calls to function fnmatch should not exist.#" + +# Local Variables: +# mode: yaml +# End: diff --git a/scripts/code-analysis b/scripts/code-analysis new file mode 100755 index 0000000..abe20a3 --- /dev/null +++ b/scripts/code-analysis @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +script_dir=$(pwd) +module_name=$(basename "$script_dir") +drupal_dir=vendor/drupal-module-code-analysis +# Relative to $drupal_dir +module_path=web/modules/contrib/$module_name + +cd "$script_dir" || exit + +drupal_composer() { + composer --working-dir="$drupal_dir" --no-interaction "$@" +} + +# Create new Drupal 10 project +if [ ! -f "$drupal_dir/composer.json" ]; then + composer --no-interaction create-project drupal/recommended-project:^10 "$drupal_dir" +fi +# Copy our code into the modules folder + +# Clean up +rm -fr "$drupal_dir/$module_path" + +# https://stackoverflow.com/a/15373763 +# rsync --archive --compress . --filter=':- .gitignore' --exclude "$drupal_dir" --exclude .git "$drupal_dir/$module_path" + +# The rsync command in not available in itkdev/php8.3-fpm + +git config --global --add safe.directory /app +# Create directories +git ls-files -z | xargs --null -I {} bash -c 'mkdir -p '"$drupal_dir/$module_path/"'$(dirname {})' +# Copy files +git ls-files -z | xargs --null -I {} cp {} "$drupal_dir/$module_path/{}" + +drupal_composer config minimum-stability dev + +# Allow ALL plugins +# https://getcomposer.org/doc/06-config.md#allow-plugins +drupal_composer config --no-plugins allow-plugins true + +drupal_composer require wikimedia/composer-merge-plugin +drupal_composer config extra.merge-plugin.include "$module_path/composer.json" +# https://www.drupal.org/project/drupal/issues/3220043#comment-14845434 +drupal_composer require --dev symfony/phpunit-bridge + +# Run PHPStan +(cd "$drupal_dir" && vendor/bin/phpstan --configuration="$module_path/phpstan.neon") diff --git a/src/ConfigHelper.php b/src/ConfigHelper.php new file mode 100644 index 0000000..8002a24 --- /dev/null +++ b/src/ConfigHelper.php @@ -0,0 +1,240 @@ +setLogger(new NullLogger()); + } + + /** + * Add enforced module dependency. + */ + public function addEnforcedDependency( + string $module, + array $configNames, + ): void { + foreach ($configNames as $name) { + $this->logger->info(sprintf('Config name: %s', $name)); + + $config = $this->configFactory->getEditable($name); + + $data = $config->getRawData(); + $data['dependencies']['enforced']['module'][] = $module; + $data['dependencies']['enforced']['module'] = array_unique($data['dependencies']['enforced']['module']); + sort($data['dependencies']['enforced']['module']); + $config->setData($data); + + $config->save(); + } + } + + /** + * Remove enforced module dependency. + */ + public function removeEnforcedDependency( + string $module, + array $configNames, + ): void { + foreach ($configNames as $name) { + $this->logger->info(sprintf('Config name: %s', $name)); + + $config = $this->configFactory->getEditable($name); + + $data = $config->getRawData(); + if (isset($data['dependencies']['enforced']['module'])) { + $data['dependencies']['enforced']['module'] = array_diff($data['dependencies']['enforced']['module'], [$module]); + sort($data['dependencies']['enforced']['module']); + if (empty($data['dependencies']['enforced']['module'])) { + unset($data['dependencies']['enforced']['module']); + } + if (empty($data['dependencies']['enforced'])) { + unset($data['dependencies']['enforced']); + } + if (empty($data['dependencies'])) { + unset($data['dependencies']); + } + $config->setData($data); + $config->save(); + } + } + } + + /** + * Write module config. + */ + public function writeModuleConfig( + string $module, + mixed $optional, + array $configNames, + ): void { + $moduleConfigPath = $this->getModuleConfigPath($module, $optional); + if (!is_dir($moduleConfigPath)) { + $this->fileSystem->mkdir($moduleConfigPath, 0755, TRUE); + } + + foreach ($configNames as $name) { + $this->logger->info($name); + + $filename = $name . '.yml'; + $destination = $moduleConfigPath . '/' . $filename; + $config = $this->configFactory->get($name)->getRawData(); + // @see https://www.drupal.org/node/2087879#s-exporting-configuration + unset($config['uuid'], $config['_core']); + + file_put_contents($destination, Yaml::encode($config)); + } + } + + /** + * Rename config. + */ + public function renameConfig( + string $from, + string $to, + array $configNames, + bool $regex = FALSE, + ): void { + $replacer = $regex + ? static fn (string $subject) => preg_replace($from, $to, $subject) + : static fn (string $subject) => str_replace($from, $to, $subject); + + foreach ($configNames as $name) { + $config = $this->configFactory->getEditable($name); + $data = $config->getRawData(); + $newData = $this->replaceKeysAndValues($replacer, $data); + if ($newData !== $data) { + $config->setData($newData); + $this->logger->info(sprintf('Saving updated config %s', $name)); + $config->save(); + } + + $newName = $replacer($name); + if ($newName !== $name) { + $this->logger->info(sprintf('Renaming config %s to %s', $name, $newName)); + $this->configFactory->rename($name, $newName); + } + } + } + + /** + * Get config names matching patterns. + */ + public function getConfigNames(array $patterns): array { + $names = $this->configFactory->listAll(); + if (empty($patterns)) { + return $names; + } + + $configNames = []; + foreach ($patterns as $pattern) { + $chunk = array_filter( + $names, + // phpcs:disable Drupal.Functions.DiscouragedFunctions.Discouraged -- https://www.php.net/manual/en/function.fnmatch.php#refsect1-function.fnmatch-notes + static fn ($name) => fnmatch($pattern, $name), + ); + if (empty($chunk)) { + throw new RuntimeException(sprintf('No config matches %s', var_export($pattern, TRUE))); + } + $configNames[] = $chunk; + } + + $configNames = array_unique(array_merge(...$configNames)); + sort($configNames); + + return $configNames; + } + + /** + * Get config names for a module. + */ + public function getModuleConfigNames(string $module): array { + return $this->getConfigNames(['*.' . $module . '.*']); + } + + /** + * Get names of config that has an enforced dependency on a module. + */ + public function getEnforcedModuleConfigNames(string $module): array { + $configNames = array_values( + array_filter( + $this->configFactory->listAll(), + function ($name) use ($module) { + $config = $this->configFactory->get($name)->getRawData(); + $list = $config['dependencies']['enforced']['module'] ?? NULL; + + return is_array($list) && in_array($module, $list, TRUE); + } + ) + ); + + if (empty($configNames)) { + throw new RuntimeException(sprintf('No config has an enforced dependency on "%s".', $module)); + } + + return $configNames; + } + + /** + * Check if a module exists. + */ + public function moduleExists(string $module): bool { + return $this->moduleHandler->moduleExists($module); + } + + /** + * Get module config path. + */ + public function getModuleConfigPath(string $module, bool $optional): string { + $modulePath = $this->moduleHandler->getModule($module)->getPath(); + + return $modulePath . '/config/' . ($optional ? 'optional' : 'install'); + } + + /** + * Replace in keys and values. + * + * @see https://stackoverflow.com/a/29619470 + */ + private function replaceKeysAndValues( + callable $replacer, + array $input, + ): array { + $return = []; + foreach ($input as $key => $value) { + $key = $replacer($key); + + if (is_array($value)) { + $value = $this->replaceKeysAndValues($replacer, $value); + } + elseif (is_string($value)) { + $value = $replacer($value); + } + + $return[$key] = $value; + } + + return $return; + } + +} diff --git a/src/Drush/Commands/ConfigHelperCommands.php b/src/Drush/Commands/ConfigHelperCommands.php new file mode 100644 index 0000000..0224dea --- /dev/null +++ b/src/Drush/Commands/ConfigHelperCommands.php @@ -0,0 +1,152 @@ +initialize(); + + $names = $this->helper->getConfigNames($patterns); + + foreach ($names as $name) { + $this->io()->writeln($name); + } + } + + /** + * Enforce module dependency in config. + * + * @see https://www.drupal.org/node/2087879#s-example:~:text=The%20dependencies%20and%20enforced%20keys%20ensure,removed%20when%20the%20module%20is%20uninstalled + */ + #[CLI\Command(name: 'config_helper:enforce-module-dependency')] + #[CLI\Argument(name: 'module', description: 'The module name.')] + #[CLI\Argument(name: 'configNames', description: 'The config names.')] + #[CLI\Usage(name: 'drush config_helper:enforce-module-dependency my_module "*.my_module.*"', description: 'Enforce dependency on my_module')] + #[CLI\Usage(name: 'drush config_helper:enforce-module-dependency my_module', description: 'Shorthand for `drush config_helper:enforce-module-dependency my_module "*.my_module.*"`')] + public function enforceModuleDependencies( + string $module, + array $configNames, + ): void { + $this->initialize(); + + if (!$this->helper->moduleExists($module)) { + throw new RuntimeException(sprintf('Invalid module: %s', $module)); + } + + $configNames = empty($configNames) + ? $this->helper->getModuleConfigNames($module) + : $this->helper->getConfigNames($configNames); + + $question = sprintf("Enforce dependency on module %s in\n * %s\n ?", $module, implode("\n * ", $configNames)); + if ($this->io()->confirm($question)) { + $this->helper->addEnforcedDependency($module, $configNames); + } + } + + /** + * Write config info config folder in module. + */ + #[CLI\Command(name: 'config_helper:write-module-config')] + #[CLI\Argument(name: 'module', description: 'The module name.')] + #[CLI\Argument(name: 'configNames', description: 'The config names.')] + #[CLI\Option(name: 'optional', description: 'Create as optional config.')] + #[CLI\Option(name: 'enforced', description: 'Move only config with enforced dependency on the module.')] + #[CLI\Usage(name: 'drush config_helper:write-module-config my_module', description: '')] + #[CLI\Usage(name: 'drush config_helper:write-module-config --source=sites/all/config my_module', description: '')] + public function writeModuleConfig( + string $module, + array $configNames, + array $options = [ + 'optional' => FALSE, + 'enforced' => FALSE, + ], + ): void { + $this->initialize(); + + if (!$this->helper->moduleExists($module)) { + throw new RuntimeException(sprintf('Invalid module: %s', $module)); + } + + if ($options['enforced']) { + $configNames = $this->helper->getEnforcedModuleConfigNames($module); + } + else { + $configNames = empty($configNames) + ? $this->helper->getModuleConfigNames($module) + : $this->helper->getConfigNames($configNames); + } + + $configPath = $this->helper->getModuleConfigPath($module, $options['optional']); + $question = sprintf("Write config\n * %s\n into %s?", implode("\n * ", $configNames), $configPath); + if ($this->io()->confirm($question)) { + $this->helper->writeModuleConfig($module, $options['optional'], $configNames); + } + } + + /** + * Rename config. + */ + #[CLI\Command(name: 'config_helper:rename')] + #[CLI\Argument(name: 'from', description: 'The value to search for.')] + #[CLI\Argument(name: 'to', description: 'The replacement value.')] + #[CLI\Argument(name: 'configNames', description: 'The config names.')] + #[CLI\Option(name: 'regex', description: 'Use regex search and replace.')] + #[CLI\Usage(name: 'drush config_helper:rename porject project', description: 'Fix typo in config.')] + #[CLI\Usage(name: "drush config_helper:rename '/field_(.+)/' '\1' --regex", description: 'Remove superfluous prefix from field machine names.')] + public function rename( + string $from, + string $to, + array $configNames, + array $options = [ + 'regex' => FALSE, + ], + ): void { + $this->initialize(); + + $configNames = $this->helper->getConfigNames($configNames); + $question = sprintf("Rename %s to %s %sin\n * %s\n?", $from, $to, $options['regex'] ? '(regex)' : '', implode("\n * ", $configNames)); + if ($this->io()->confirm($question)) { + $this->helper->renameConfig($from, $to, $configNames, (bool) $options['regex']); + } + } + + /** + * Initialize. + * + * @todo Can we automatically call this before each command? + */ + private function initialize(): void { + $this->helper->setLogger($this->logger()); + } + +}