Переглянути джерело

Simplify command writing by refactoring most of their logic.

- BuildSettings and HugoConfig were roughly reduced by 50%
- PHPCS now applies Drupal coding standards.
Frederic G. MARAND 5 роки тому
батько
коміт
b740a63f7c

+ 4 - 2
composer.json

@@ -23,9 +23,11 @@
   },
   "type": "composer-plugin",
   "require-dev": {
-    "squizlabs/php_codesniffer": "^3.0@dev"
+    "squizlabs/php_codesniffer": "^3.0@dev",
+    "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+    "drupal/coder": "^8.3.6"
   },
   "scripts": {
-    "cs": "phpcs --standard=PSR1,PSR2 -v src"
+    "cs": "phpcs --standard=PSR1,Drupal -v src"
   }
 }

+ 138 - 22
src/BaseBuilderCommand.php

@@ -1,4 +1,5 @@
 <?php
+
 declare(strict_types = 1);
 
 namespace Fgm\Drupal\Composer;
@@ -6,30 +7,43 @@ namespace Fgm\Drupal\Composer;
 use Composer\Command\BaseCommand;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Yaml\Exception\ParseException;
+use Symfony\Component\Yaml\Yaml;
+use Twig\Environment;
+use Twig\Extension\DebugExtension;
+use Twig\Loader\FilesystemLoader;
+use Twig\TemplateWrapper;
 
-abstract class BaseBuilderCommand extends BaseCommand
-{
-
-    /**
-     * The name of the argument defining the file to generate.
-     */
-    const ARG_FILE = 'file';
+/**
+ * Provides command primitives useful for most commands, focused on templating.
+ *
+ * @package Fgm\Drupal\Composer
+ */
+abstract class BaseBuilderCommand extends BaseCommand {
 
-    protected function getBuilderConfig(): array
-    {
-        $conf = $this->getComposer()->getPackage()->getExtra()[Builder::NAME] ?? [];
-        return $conf;
-    }
+  /**
+   * The name of the argument defining the file to generate.
+   */
+  const ARG_FILE = 'file';
 
-    protected function getSettingsPath(): string
-    {
-        $settingsPath = getcwd() . "/settings";
-        return $settingsPath;
-    }
+  /**
+   * Provide the path to the project settings directory.
+   *
+   * @return string
+   *   The absolute path to the directory.
+   */
+  protected function getSettingsPath(): string {
+    $settingsPath = getcwd() . "/settings";
+    return $settingsPath;
+  }
 
   /**
+   * Validate template definition for command, returning its name on success.
+   *
    * @param \Symfony\Component\Console\Input\InputInterface $input
+   *   Command input.
    * @param \Symfony\Component\Console\Output\OutputInterface $output
+   *   Command output.
    *
    * @return array
    *   - string Template name
@@ -37,15 +51,117 @@ abstract class BaseBuilderCommand extends BaseCommand
    */
   public function validateTemplateName(InputInterface $input, OutputInterface $output): array {
     $file = $input->getArgument(static::ARG_FILE);
-    $conf = $this->getBuilderConfig();
+    $conf = $this->getComposer()->getPackage()->getExtra()[Builder::NAME] ?? [];
     $templateName = $conf['templates'][$file] ?? '';
     if (empty($templateName)) {
-        $output->writeln(sprintf(
-            'Could not build file %s: no such template in composer.json extra section',
-          $file
-        ));
+      $output->writeln(sprintf(
+        'Could not build file %s: no such template in composer.json extra section',
+        $file
+      ));
       return ["", 1];
     }
     return [$templateName, 0];
   }
+
+  /**
+   * Perform template rendering.
+   *
+   * @param \Twig\TemplateWrapper $wrapper
+   *   A Twig user-space template wrapper.
+   * @param array $context
+   *   The data context with which to perform the rendering.
+   * @param string $destination
+   *   The path where to write the rendering result.
+   *
+   * @return array
+   *   - string message
+   *   - int status: 0 on success, other values on errors.
+   */
+  protected function render(
+    TemplateWrapper $wrapper,
+    array $context,
+    string $destination
+  ): array {
+    if (file_exists($destination)) {
+      $ok = unlink($destination);
+      if (!$ok) {
+        return [sprintf("Could not remove old %s file", $destination), 1];
+      }
+    }
+
+    $rendered = $wrapper->render($context);
+    $ok = file_put_contents($destination, $rendered, LOCK_EX);
+    if (!$ok) {
+      return [sprintf('Could not write new %s file', $destination), 2];
+    }
+
+    return ["", 0];
+  }
+
+  /**
+   * Prepare the template and its parameters as obtained from configuration.
+   *
+   * @param \Symfony\Component\Console\Input\InputInterface $input
+   *   Command input.
+   * @param \Symfony\Component\Console\Output\OutputInterface $output
+   *   Command output.
+   *
+   * @return array
+   *   - TemplateWrapper|NULL: Twig template wrapper
+   *   - array: template parameters
+   *   - string: error message
+   *   - int: error, 0 if OK. If non-zero, only the error message is reliable.
+   *
+   * @throws \Twig\Error\LoaderError
+   * @throws \Twig\Error\RuntimeError
+   * @throws \Twig\Error\SyntaxError
+   */
+  protected function prepare(InputInterface $input, OutputInterface $output): array {
+    [$templateName, $err] = $this->validateTemplateName($input, $output);
+    if ($err) {
+      return [NULL, [], "Could not validate template name", 1];
+    };
+
+    $settingsPath = $this->getSettingsPath();
+    $templatePath = "${settingsPath}/${templateName}";
+    $realTemplatePath = realpath($templatePath);
+    if (empty($realTemplatePath)) {
+      return [NULL, [],
+        sprintf("Could not load template %s: no such file", $templateName),
+        2,
+      ];
+    }
+
+    $paramsPath = "${settingsPath}/merged.params.local.yml";
+    $realParamsPath = realpath($paramsPath);
+    if (empty($realParamsPath)) {
+      return [NULL, [],
+        sprintf("Could not load parameters %s: no such file", $paramsPath),
+        3,
+      ];
+    }
+
+    $yaml = new Yaml();
+    try {
+      $params = $yaml->parseFile($realParamsPath);
+    }
+    catch (ParseException $e) {
+      return [NULL, [],
+        sprintf("Could not parse %s: %s", $realParamsPath, $e->getMessage()),
+        4,
+      ];
+    }
+
+    $loader = new FilesystemLoader($settingsPath, $settingsPath);
+    $twig = new Environment($loader, [
+      'auto_reload' => TRUE,
+      'cache' => FALSE,
+      'debug' => TRUE,
+      'strict_variables' => TRUE,
+    ]);
+    $twig->addExtension(new DebugExtension());
+    $wrapper = $twig->load($templateName);
+    return [$wrapper, $params, "", 0];
+  }
+
 }

+ 66 - 118
src/BuildSettingsCommand.php

@@ -1,4 +1,5 @@
 <?php
+
 declare(strict_types = 1);
 
 namespace Fgm\Drupal\Composer;
@@ -7,133 +8,80 @@ use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputDefinition;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Yaml\Exception\ParseException;
-use Symfony\Component\Yaml\Yaml;
-use Twig\Environment;
-use Twig\Extension\DebugExtension;
-use Twig\Loader\FilesystemLoader;
-use Twig\TemplateWrapper;
-
-class BuildSettingsCommand extends BaseBuilderCommand
-{
 
-    /**
-     * @var string
-     */
-    protected $eventName;
-
-    /**
-     * {@inheritDoc}
-     */
-    public function configure()
-    {
-        parent::configure();
-        $this->eventName = $this->getName();
-        $this
-            ->setName('build:settings')
-            ->setDescription('Step 4: build the *.settings.local.php files.')
-            ->setDefinition(
-                new InputDefinition([
-                new InputArgument(static::ARG_FILE, InputArgument::OPTIONAL),
-                ])
-            )
-            ->setHelp(
-                <<<EOT
+/**
+ * Build the build- and run-time settings for all sites in the multisite.
+ *
+ * @package Fgm\Drupal\Composer
+ */
+class BuildSettingsCommand extends BaseBuilderCommand {
+
+  /**
+   * The event triggering this command.
+   *
+   * @var string
+   */
+  protected $eventName;
+
+  /**
+   * Configures the current command.
+   */
+  public function configure() {
+    parent::configure();
+    $this->eventName = $this->getName();
+    $this
+      ->setName('build:settings')
+      ->setDescription('Step 4: build the *.settings.local.php files.')
+      ->setDefinition(
+        new InputDefinition([
+          new InputArgument(static::ARG_FILE, InputArgument::OPTIONAL),
+        ])
+      )
+      ->setHelp(
+        <<<EOT
 The build:settings command combines shared and per-environment parameters and passes
 them to the settings.local.php.twig template to build the settings/(build|run).settings.local.php files.
 
 EOT
-            );
+      );
+  }
+
+  /**
+   * Executes the current command.
+   *
+   * {@inheritDoc}
+   */
+  public function execute(InputInterface $input, OutputInterface $output): int {
+    [$wrapper, $params, $msg, $err] = $this->prepare($input, $output);
+    if ($err != 0) {
+      $output->writeln($msg);
+      return $err;
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    public function execute(InputInterface $input, OutputInterface $output)
-    {
-        [$templateName, $err] = $this->validateTemplateName($input, $output);
-        if ($err) {
-          return 1;
-        };
-
-        $settingsPath = $this->getSettingsPath();
-        $templatePath = "${settingsPath}/${templateName}";
-        $realTemplatePath = realpath($templatePath);
-        if (empty($realTemplatePath)) {
-            $output->writeln(sprintf("Could not load template %s: no such file", $templateName));
-            return 2;
-        }
-
-        $paramsPath = "${settingsPath}/merged.params.local.yml";
-        $realParamsPath = realpath($paramsPath);
-        if (empty($realParamsPath)) {
-            $output->writeln(sprintf("Could not load parameters %s: no such file", $paramsPath));
-            return 3;
-        }
-
-        $yaml = new Yaml();
-        try {
-            $params = $yaml->parseFile($realParamsPath);
-        } catch (ParseException $e) {
-            $output->writeln(sprintf("Could not parse %s: %s", $realParamsPath, $e->getMessage()));
-            return 4;
-        }
-
-        $loader = new FilesystemLoader($settingsPath, $settingsPath);
-        $twig = new Environment($loader, [
-          'auto_reload' => true,
-          'cache' => false,
-          'debug' => true,
-          'strict_variables' => true,
-        ]);
-        $twig->addExtension(new DebugExtension());
-
-        $wrapper = $twig->load($templateName);
-        foreach ($params['sites'] as $name => $siteParams) {
-            foreach (['build', 'run'] as $stage) {
-                $stageSettings = NestedArray::mergeDeepArray([
-                  $siteParams['settings']['both'] ?? [],
-                  $siteParams['settings'][$stage] ?? []
-                ], true);
-                $stageParams = $siteParams;
-                $stageParams['settings'] = $stageSettings;
-                $context = [
-                  'instance' => $params['instance'],
-                  'name' => $name,
-                  'stage' => $stage,
-                  'site' => $stageParams,
-                ];
-                $error = $this->render($wrapper, $context, $output);
-                if ($error) {
-                    $output->writeln(sprintf("Failed rendering %s settings for site %s", $stage, $name));
-                    return 5;
-                }
-            }
+    foreach ($params['sites'] as $name => $siteParams) {
+      foreach (['build', 'run'] as $stage) {
+        $stageSettings = NestedArray::mergeDeepArray([
+          $siteParams['settings']['both'] ?? [],
+          $siteParams['settings'][$stage] ?? [],
+        ], TRUE);
+        $stageParams = $siteParams;
+        $stageParams['settings'] = $stageSettings;
+        $context = [
+          'instance' => $params['instance'],
+          'name' => $name,
+          'stage' => $stage,
+          'site' => $stageParams,
+        ];
+        $destination = "web/sites/${context['name']}/${context['stage']}.settings.local.php";
+        [$msg, $error] = $this->render($wrapper, $context, $destination);
+        if ($error) {
+          $output->writeln(sprintf("Failed rendering %s settings for site %s: %s", $stage, $name, $msg));
+          return 5;
         }
+      }
     }
 
-    protected function render(TemplateWrapper $wrapper, array $context, OutputInterface $output): int
-    {
-        $settingsPath = "web/sites/${context['name']}/${context['stage']}.settings.local.php";
-        if (file_exists($settingsPath)) {
-            $ok = unlink($settingsPath);
-            if (!$ok) {
-                $output->writeln(sprintf(
-                    "Could not remove old %s file",
-                    $settingsPath
-                ));
-                return 1;
-            }
-        }
-
-        $rendered = $wrapper->render($context);
-        $ok = file_put_contents($settingsPath, $rendered, LOCK_EX);
-        if (!$ok) {
-            $output->writeln(sprintf('Could not write new %s file', $settingsPath));
-            return 2;
-        }
-
-        return 0;
-    }
+    return 0;
+  }
 
 }

+ 78 - 71
src/Builder.php

@@ -1,4 +1,5 @@
 <?php
+
 declare(strict_types = 1);
 
 namespace Fgm\Drupal\Composer;
@@ -13,83 +14,89 @@ use Composer\Plugin\PluginInterface;
 use Composer\Script\Event as ScriptEvent;
 use Composer\Script\ScriptEvents;
 
-class Builder implements Capable, Capability, EventSubscriberInterface, PluginInterface
-{
+/**
+ * Class Builder is the Composer plugin entry point.
+ *
+ * @package Fgm\Drupal\Composer
+ */
+class Builder implements Capable, Capability, EventSubscriberInterface, PluginInterface {
 
-    const NAME = "composer_builder";
+  const NAME = "composer_builder";
 
-    /**
-     * @var \Composer\Composer
-     */
-    protected $composer;
+  /**
+   * The currently running Composer instance.
+   *
+   * @var \Composer\Composer
+   */
+  protected $composer;
 
-    /**
-     * @var \Composer\IO\IOInterface
-     */
-    protected $io;
+  /**
+   * The Composer I/O.
+   *
+   * @var \Composer\IO\IOInterface
+   */
+  protected $io;
 
-    /**
-     * {@inheritdoc}
-     *
-     * Available events:
-     *
-     * - Composer\Installer\InstallerEvents::* ->
-     * Composer\Installer\InstallerEvent
-     * - Composer\Installer\PackageEvents::* -> Composer\Installer\PackageEvent
-     * - Composer\Installer\PluginEvents::INIT -> Composer\EventDispatcher\Event
-     * - Composer\Installer\PluginEvents::COMMAND -> Composer\Plugin\CommandEvent
-     * - Composer\Installer\PluginEvents::PRE_FILE_DOWNLOAD
-     *     -> Composer\Plugin\PreFileDownloadEvent
-     * - Composer\Script\ScriptEvents::* -> Composer\Script\Event
-     */
-    public static function getSubscribedEvents()
-    {
-        return [
-          ScriptEvents::POST_INSTALL_CMD => 'onScriptEvent',
-          ScriptEvents::POST_UPDATE_CMD => 'onScriptEvent',
-        ];
-    }
+  /**
+   * {@inheritdoc}
+   *
+   * Available events:
+   *
+   * - Composer\Installer\InstallerEvents::*
+   *    -> Composer\Installer\InstallerEvent
+   * - Composer\Installer\PackageEvents::* -> Composer\Installer\PackageEvent
+   * - Composer\Installer\PluginEvents::INIT -> Composer\EventDispatcher\Event
+   * - Composer\Installer\PluginEvents::COMMAND -> Composer\Plugin\CommandEvent
+   * - Composer\Installer\PluginEvents::PRE_FILE_DOWNLOAD
+   *     -> Composer\Plugin\PreFileDownloadEvent
+   * - Composer\Script\ScriptEvents::* -> Composer\Script\Event
+   */
+  public static function getSubscribedEvents() {
+    return [
+      ScriptEvents::POST_INSTALL_CMD => 'onScriptEvent',
+      ScriptEvents::POST_UPDATE_CMD => 'onScriptEvent',
+    ];
+  }
 
-    /**
-     * Apply plugin modifications to Composer
-     *
-     * @param Composer    $composer
-     * @param IOInterface $io
-     */
-    public function activate(Composer $composer, IOInterface $io)
-    {
-        $this->composer = $composer;
-        $this->io = $io;
-    }
+  /**
+   * Apply plugin modifications to Composer.
+   *
+   * @param \Composer\Composer $composer
+   *   The currently running Composer instance.
+   * @param \Composer\IO\IOInterface $io
+   *   The Composer I/O.
+   */
+  public function activate(Composer $composer, IOInterface $io) {
+    $this->composer = $composer;
+    $this->io = $io;
+  }
 
-    /**
-     * {@inheritDoc}
-     */
-    public function getCapabilities()
-    {
-        return [
-          CommandProvider::class => BuilderCommandProvider::class,
-        ];
-    }
+  /**
+   * Composer plugin API: describe the plugin capabilities.
+   */
+  public function getCapabilities() {
+    return [
+      CommandProvider::class => BuilderCommandProvider::class,
+    ];
+  }
 
-    /**
-     * Event callback: run build:settings  on post-install|update only.
-     *
-     * @param \Composer\Script\Event $event
-     *
-     * @throws \Exception
-     */
-    public function onScriptEvent(ScriptEvent $event)
-    {
-        if (in_array($event->getName(), [
-            ScriptEvents::POST_INSTALL_CMD,
-            ScriptEvents::POST_UPDATE_CMD,
-          ]
-        )
-        ) {
-            // FIXME add behavior where, without an argument, the command will build all templates.
-            // $buildCommand = new BuildSettingsCommand($event->getName());
-            // $buildCommand->run(new ArgvInput([]), new ConsoleOutput());
-        }
+  /**
+   * Event callback: run build:settings  on post-install|update only.
+   *
+   * @param \Composer\Script\Event $event
+   *   The subscribed event triggering this callback.
+   *
+   * @throws \Exception
+   */
+  public function onScriptEvent(ScriptEvent $event) {
+    if (in_array($event->getName(), [
+      ScriptEvents::POST_INSTALL_CMD,
+      ScriptEvents::POST_UPDATE_CMD,
+    ])) {
+      // FIXME without an argument, the command should build all templates.
+      // $buildCommand = new BuildSettingsCommand($event->getName());
+      // $buildCommand->run(new ArgvInput([]), new ConsoleOutput());
     }
+  }
+
 }

+ 52 - 44
src/BuilderCommandProvider.php

@@ -1,52 +1,60 @@
 <?php
+
 declare(strict_types = 1);
 
 namespace Fgm\Drupal\Composer;
 
 use Composer\Plugin\Capability\CommandProvider;
 
-class BuilderCommandProvider implements CommandProvider
-{
-
-    /**
-     * @var \Composer\Composer
-     */
-    protected $composer;
-
-    /**
-     * @var \Composer\IO\IOInterface
-     */
-    protected $io;
-
-    /**
-     * An instance of the plugin instantiated not-as-a-command.
-     *
-     * @var self
-     */
-    protected $plugin;
-
-    /**
-     * BuilderCommandProvider constructor.
-     *
-     * @param array|NULL $args
-     *   Guaranteed to contain composer/io/plugin as per CommandProvider.
-     */
-    public function __construct(array $args = null)
-    {
-        $this->composer = $args['composer'];
-        $this->io = $args['io'];
-        $this->plugin = $args['plugin'];
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getCommands()
-    {
-        return [
-            new BuildSettingsCommand(),
-            new HugoConfigCommand(),
-            new MergeParamsCommand(),
-        ];
-    }
+/**
+ * Command capability for Composer plugins: provides the list of commmands.
+ *
+ * @package Fgm\Drupal\Composer
+ */
+class BuilderCommandProvider implements CommandProvider {
+
+  /**
+   * The currently running Composer instance.
+   *
+   * @var \Composer\Composer
+   */
+  protected $composer;
+
+  /**
+   * The Composer I/O.
+   *
+   * @var \Composer\IO\IOInterface
+   */
+  protected $io;
+
+  /**
+   * An instance of the plugin instantiated not-as-a-command.
+   *
+   * @var self
+   */
+  protected $plugin;
+
+  /**
+   * BuilderCommandProvider constructor.
+   *
+   * @param array|null $args
+   *   Guaranteed to contain composer/io/plugin as per CommandProvider.
+   */
+  public function __construct(array $args = NULL) {
+    $this->composer = $args['composer'];
+    $this->io = $args['io'];
+    $this->plugin = $args['plugin'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCommands() {
+    return [
+      new BuildSettingsCommand(),
+      new HugoConfigCommand(),
+      new MergeParamsCommand(),
+    ];
+  }
+
 }

+ 59 - 110
src/HugoConfigCommand.php

@@ -1,4 +1,5 @@
 <?php
+
 declare(strict_types = 1);
 
 namespace Fgm\Drupal\Composer;
@@ -7,129 +8,77 @@ use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputDefinition;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Yaml\Exception\ParseException;
-use Symfony\Component\Yaml\Yaml;
-use Twig\Environment;
-use Twig\Extension\DebugExtension;
-use Twig\Loader\FilesystemLoader;
-use Twig\TemplateWrapper;
-
-class HugoConfigCommand extends BaseBuilderCommand
-{
 
-    /**
-     * @var string
-     */
-    protected $eventName;
+/**
+ * Configure the Hugo-based documentation site for the project.
+ *
+ * @package Fgm\Drupal\Composer
+ */
+class HugoConfigCommand extends BaseBuilderCommand {
+
+  /**
+   * The event triggering this command.
+   *
+   * @var string
+   */
+  protected $eventName;
 
-    /**
-     * {@inheritDoc}
-     */
-    public function configure()
-    {
-        parent::configure();
-        $this->eventName = $this->getName();
-        $this
-            ->setName('build:config_doc')
-            ->setDescription('Step 2 (Optional): configure the documentation site for the current environment.')
-            ->setDefinition(
-                new InputDefinition([
-                new InputArgument(static::ARG_FILE, InputArgument::OPTIONAL),
-                ])
-            )
-            ->setHelp(
-                <<<EOT
+  /**
+   * Configures the current command.
+   */
+  public function configure() {
+    parent::configure();
+    $this->eventName = $this->getName();
+    $this
+      ->setName('build:config_doc')
+      ->setDescription('Step 2 (Optional): configure the documentation site for the current environment.')
+      ->setDefinition(
+        new InputDefinition([
+          new InputArgument(static::ARG_FILE, InputArgument::OPTIONAL),
+        ])
+      )
+      ->setHelp(
+        <<<EOT
 The build:config_doc modifies the doc/config.toml file and rebuilds the documentation
 site to account for the changes.
 
 EOT
-            );
-    }
+      );
+  }
 
-    /**
+  /**
+   * Executes the current command.
+   *
    * {@inheritDoc}
    */
-    public function execute(InputInterface $input, OutputInterface $output)
-    {
-        [$templateName, $err] = $this->validateTemplateName($input, $output);
-        if ($err) {
-            return 1;
-        };
-
-        $settingsPath = $this->getSettingsPath();
-        $templatePath = "${settingsPath}/${templateName}";
-        $realTemplatePath = realpath($templatePath);
-        if (empty($realTemplatePath)) {
-          $output->writeln(sprintf("Could not load template %s: no such file", $templateName));
-          return 2;
-        }
-
-        $paramsPath = "${settingsPath}/merged.params.local.yml";
-        $realParamsPath = realpath($paramsPath);
-        if (empty($realParamsPath)) {
-            $output->writeln(sprintf("Could not load parameters %s: no such file", $paramsPath));
-            return 3;
-        }
-        $yaml = new Yaml();
-
-        try {
-            $params = $yaml->parseFile($realParamsPath);
-        } catch (ParseException $e) {
-            $output->writeln(sprintf("Could not parse %s: %s", $realParamsPath, $e->getMessage()));
-            return 4;
-        }
-
-        $loader = new FilesystemLoader($settingsPath, $settingsPath);
-        $twig = new Environment($loader, [
-            'auto_reload' => true,
-            'cache' => false,
-            'debug' => true,
-            'strict_variables' => true,
-        ]);
-        $twig->addExtension(new DebugExtension());
-
-        $wrapper = $twig->load($templateName);
-        $context = [
-          'base_url' => $params['instance']['doc']['base_url'],
-        ];
-        $error = $this->render($wrapper, $context, $output);
-        if ($error) {
-          $output->writeln(sprintf("Failed rendering doc configuration"));
-          return 5;
-        }
-
-        $cwd = getcwd();
-        chdir('doc');
-        $err = system("hugo -D", $exit);
-        if ($exit != 0) {
-          $output->writeln(sprintf("Failed running hugo to rebuild documentation site: %s\n", $err));
-          return 6;
-        }
-        chdir($cwd);
+  public function execute(InputInterface $input, OutputInterface $output): int {
+    [$wrapper, $params, $msg, $err] = $this->prepare($input, $output);
+    if ($err != 0) {
+      $output->writeln($msg);
+      return $err;
     }
 
-  protected function render(TemplateWrapper $wrapper, array $context, OutputInterface $output): int
-    {
-        $configPath = "doc/config.toml";
-        if (file_exists($configPath)) {
-          $ok = unlink($configPath);
-          if (!$ok) {
-            $output->writeln(sprintf(
-              "Could not remove old %s file",
-              $configPath
-            ));
-            return 1;
-          }
-        }
+    $context = [
+      'base_url' => $params['instance']['doc']['base_url'],
+    ];
 
-        $rendered = $wrapper->render($context);
-        $ok = file_put_contents($configPath, $rendered, LOCK_EX);
-        if (!$ok) {
-          $output->writeln(sprintf('Could not write new %s file', $configPath));
-          return 2;
-        }
+    $destination = "doc/config.toml";
+    [$msg, $error] = $this->render($wrapper, $context, $destination);
+    if ($error) {
+      $output->writeln(sprintf("Failed rendering doc configuration: %s", $msg));
+      return 5;
+    }
 
-        return 0;
+    $cwd = getcwd();
+    chdir('doc');
+    $err = system("hugo -D", $exit);
+    if ($exit != 0) {
+      $output->writeln(sprintf("Failed running hugo to rebuild documentation site: %s\n", $err));
+      return 6;
     }
+    chdir($cwd);
+
+    return 0;
+  }
 
 }

+ 94 - 71
src/MergeParamsCommand.php

@@ -1,4 +1,5 @@
 <?php
+
 declare(strict_types = 1);
 
 namespace Fgm\Drupal\Composer;
@@ -7,91 +8,113 @@ use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Yaml\Yaml;
 
-class MergeParamsCommand extends BaseBuilderCommand
-{
-    const DEFAULT_SITE = '_default';
+/**
+ * Merge params.local.yml into dist.params.local.yml.
+ *
+ * The former overrides the latter.
+ *
+ * @package Fgm\Drupal\Composer
+ */
+class MergeParamsCommand extends BaseBuilderCommand {
+
+  const DEFAULT_SITE = '_default';
 
-    /**
+  /**
+   * The event triggering this command.
+   *
    * @var string
    */
-    protected $eventName;
+  protected $eventName;
 
-    /**
-   * {@inheritDoc}
+  /**
+   * Configures the current command.
    */
-    public function configure()
-    {
-        parent::configure();
-        $this->eventName = $this->getName();
-        $this
-            ->setName('build:merge_params')
-            ->setDescription('Step 1: merges the params.local.yml with dist.params.local.yml.')
-            ->setDefinition([])
-            ->setHelp(
-                <<<EOT
+  public function configure() {
+    parent::configure();
+    $this->eventName = $this->getName();
+    $this
+      ->setName('build:merge_params')
+      ->setDescription('Step 1: merges the params.local.yml with dist.params.local.yml.')
+      ->setDefinition([])
+      ->setHelp(
+        <<<EOT
 Step 1 of a build, the build:merge_params command combines shared and per-environment
 parameters and generates merged.params.local.yml containing the merged result.
 
 EOT
-            );
+      );
+  }
+
+  /**
+   * Perform merging.
+   *
+   * @param array $dist
+   *   Distribution parameters (default).
+   * @param array $local
+   *   Local parameters (overrides).
+   *
+   * @return array
+   *   The merge result.
+   */
+  protected function merge(array $dist, array $local): array {
+    if (empty($local)) {
+      return $dist;
     }
 
-    protected function merge(array $dist, array $local): array
-    {
-        if (empty($local)) {
-            return $dist;
-        }
-
-        // Merge local into defaults.
-        $merged = NestedArray::mergeDeepArray([$dist, $local], true);
-
-        // Generate per-site data from settings/_default.
-        $default = $merged['sites'][static::DEFAULT_SITE] ?? [];
-        $sites = array_diff_key($merged['sites'], [static::DEFAULT_SITE => null]);
-        $merged['sites'] = [];
-        foreach ($sites as $name => $params) {
-            $merged['sites'][$name] = NestedArray::mergeDeep($default, $params);
-        }
-        return $merged;
+    // Merge local into defaults.
+    $merged = NestedArray::mergeDeepArray([$dist, $local], TRUE);
+
+    // Generate per-site data from settings/_default.
+    $default = $merged['sites'][static::DEFAULT_SITE] ?? [];
+    $sites = array_diff_key($merged['sites'], [static::DEFAULT_SITE => NULL]);
+    $merged['sites'] = [];
+    foreach ($sites as $name => $params) {
+      $merged['sites'][$name] = NestedArray::mergeDeep($default, $params);
     }
+    return $merged;
+  }
 
-    /**
+  /**
+   * Executes the current command.
+   *
    * {@inheritDoc}
    */
-    public function execute(InputInterface $input, OutputInterface $output)
-    {
-        $settingsPath = $this->getSettingsPath();
-        $yaml = new Yaml();
-
-        // Load defaults.
-        $defaultsPath = "${settingsPath}/dist.params.local.yml";
-        $realDefaultsPath = realpath($defaultsPath);
-        if (empty($realDefaultsPath)) {
-            $output->writeln("Failed to open $defaultsPath");
-            return 1;
-        }
-        $defaults = $yaml->parseFile($realDefaultsPath);
-
-        // Load local.
-        $localPath = "${settingsPath}/params.local.yml";
-        $realLocalPath = realpath($localPath);
-        if (empty($realLocalPath)) {
-            if ($output->isVerbose()) {
-                $output->writeln("File $localPath not found, using only defaults");
-            }
-            $local = [];
-        } else {
-            $local = $yaml->parseFile($realLocalPath);
-        }
-
-        // Merge.
-        $merged = $this->merge($defaults, $local);
-
-        // Write.
-        $ok = file_put_contents("${settingsPath}/merged.params.local.yml", $yaml->dump($merged, 10, 2));
-        if (!$ok) {
-            $output->writeln("Failed to write merged params.");
-            return 2;
-        }
+  public function execute(InputInterface $input, OutputInterface $output) {
+    $settingsPath = $this->getSettingsPath();
+    $yaml = new Yaml();
+
+    // Load defaults.
+    $defaultsPath = "${settingsPath}/dist.params.local.yml";
+    $realDefaultsPath = realpath($defaultsPath);
+    if (empty($realDefaultsPath)) {
+      $output->writeln("Failed to open $defaultsPath");
+      return 1;
     }
+    $defaults = $yaml->parseFile($realDefaultsPath);
+
+    // Load local.
+    $localPath = "${settingsPath}/params.local.yml";
+    $realLocalPath = realpath($localPath);
+    if (empty($realLocalPath)) {
+      if ($output->isVerbose()) {
+        $output->writeln("File $localPath not found, using only defaults");
+      }
+      $local = [];
+    }
+    else {
+      $local = $yaml->parseFile($realLocalPath);
+    }
+
+    // Merge.
+    $merged = $this->merge($defaults, $local);
+
+    // Write.
+    $ok = file_put_contents("${settingsPath}/merged.params.local.yml",
+      $yaml->dump($merged, 10, 2));
+    if (!$ok) {
+      $output->writeln("Failed to write merged params.");
+      return 2;
+    }
+  }
+
 }

+ 2 - 0
src/NestedArray.php

@@ -1,5 +1,7 @@
 <?php
 
+// @codingStandardsIgnoreFile
+
 /**
  * @file
  * This is a copy of <Drupal 8.8>/core/lib/Drupal/Component/Utility/NestedArray.php