config_preview_deploy-1.0.0-alpha3/src/Drush/Commands/ConfigPreviewDeployCommands.php

src/Drush/Commands/ConfigPreviewDeployCommands.php
<?php

declare(strict_types=1);

namespace Drupal\config_preview_deploy\Drush\Commands;

use Drupal\config_preview_deploy\ConfigDiff;
use Drupal\config_preview_deploy\ProductionConfigDeployer;
use Drupal\config_preview_deploy\ConfigVerifier;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Drush commands for Config Preview Deploy.
 */
final class ConfigPreviewDeployCommands extends DrushCommands {

  /**
   * The configuration diff service.
   */
  protected ConfigDiff $configDiff;

  /**
   * The config verifier service.
   */
  protected ConfigVerifier $configVerifier;

  /**
   * The production config deployer service.
   */
  protected ProductionConfigDeployer $productionConfigDeployer;

  /**
   * The date formatter service.
   */
  protected DateFormatterInterface $dateFormatter;

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

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

  /**
   * Constructs a ConfigPreviewDeployCommands object.
   */
  public function __construct(
    ConfigDiff $configDiff,
    ConfigVerifier $configVerifier,
    ProductionConfigDeployer $productionConfigDeployer,
    DateFormatterInterface $dateFormatter,
    ConfigFactoryInterface $configFactory,
    EntityTypeManagerInterface $entityTypeManager,
  ) {
    parent::__construct();
    $this->configDiff = $configDiff;
    $this->configVerifier = $configVerifier;
    $this->productionConfigDeployer = $productionConfigDeployer;
    $this->dateFormatter = $dateFormatter;
    $this->configFactory = $configFactory;
    $this->entityTypeManager = $entityTypeManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config_preview_deploy.config_diff'),
      $container->get('config_preview_deploy.config_verifier'),
      $container->get('config_preview_deploy.production_config_deployer'),
      $container->get('date.formatter'),
      $container->get('config.factory'),
      $container->get('entity_type.manager')
    );
  }

  /**
   * Initialize preview environment after production sync.
   */
  #[CLI\Command(name: 'config:preview-deploy:init', aliases: ['cpd:init'])]
  #[CLI\Help(description: 'Initialize preview environment after production sync by creating base checkpoint.')]
  public function init(): void {
    try {
      $checkpointId = $this->configDiff->createBaseCheckpoint();

      $this->output()->writeln(dt('<info>Base checkpoint created: @id</info>', [
        '@id' => $checkpointId,
      ]));

      $baseVersion = $this->configDiff->getBaseVersion();
      $this->output()->writeln(dt('<info>Base version timestamp: @date</info>', [
        '@date' => $this->dateFormatter->format($baseVersion['timestamp'], 'custom', 'Y-m-d H:i:s'),
      ]));

      $this->output()->writeln(dt('<comment>Preview environment is now ready for configuration changes.</comment>'));

    }
    catch (\Exception $e) {
      $this->output()->writeln(dt('<error>Failed to initialize: @message</error>', [
        '@message' => $e->getMessage(),
      ]));
      throw $e;
    }
  }

  /**
   * Generate and display configuration diff.
   */
  #[CLI\Command(name: 'config:preview-deploy:diff', aliases: ['cpd:diff'])]
  #[CLI\Help(description: 'Generate and display configuration diff from base checkpoint.')]
  #[CLI\Option(name: 'output', description: 'Save diff to file instead of displaying.')]
  #[CLI\Option(name: 'no-filter', description: 'Disable config transformations (e.g., config_ignore filtering).')]
  public function diff(array $options = ['output' => NULL, 'no-filter' => FALSE]): void {
    try {
      // Apply transformations by default, disable if --no-filter is set.
      $applyTransformations = !$options['no-filter'];
      $diff = $this->configDiff->generateDiff($applyTransformations);

      if (empty($diff)) {
        $this->output()->writeln(dt('<comment>No configuration changes detected.</comment>'));
        return;
      }

      // Get changed configs respecting the filter option.
      $changedConfigs = $this->configDiff->getChangedConfigs($applyTransformations);
      $this->output()->writeln(dt('<info>Found @count changed configuration(s):</info>', [
        '@count' => count($changedConfigs),
      ]));

      foreach ($changedConfigs as $config) {
        $this->output()->writeln(dt('- @config', ['@config' => $config]));
      }

      if ($options['output']) {
        file_put_contents($options['output'], $diff);
        $this->output()->writeln(dt('<info>Diff saved to: @file</info>', [
          '@file' => $options['output'],
        ]));
      }
      else {
        $this->output()->writeln(dt("\n<comment>Configuration diff:</comment>"));
        $this->output()->writeln($diff);
      }

    }
    catch (\Exception $e) {
      $this->output()->writeln(dt('<error>Failed to generate diff: @message</error>', [
        '@message' => $e->getMessage(),
      ]));
      throw $e;
    }
  }

  /**
   * Apply configuration diff.
   */
  #[CLI\Command(name: 'config:preview-deploy:apply', aliases: ['cpd:apply'])]
  #[CLI\Help(description: 'Apply configuration diff on production (production environment only).')]
  #[CLI\Argument(name: 'diffFile', description: 'Path to diff file to apply.')]
  #[CLI\Option(name: 'environment', description: 'Source environment name.')]
  public function apply(string $diffFile, array $options = ['environment' => 'unknown']): int {
    try {
      if (!file_exists($diffFile)) {
        throw new \InvalidArgumentException(dt('Diff file not found: @file', [
          '@file' => $diffFile,
        ]));
      }

      $diff = file_get_contents($diffFile);
      $environment = $options['environment'];

      // Show filename instead of "unknown" when environment is not specified.
      $sourceLabel = ($environment === 'unknown') ? basename($diffFile) : $environment;
      $this->output()->writeln(dt('<comment>Applying diff from @source...</comment>', [
        '@source' => $sourceLabel,
      ]));

      // Create checkpoint before applying.
      $checkpointLabel = dt('Manual apply from @source - @date', [
        '@source' => $sourceLabel,
        '@date' => date('Y-m-d H:i:s'),
      ]);
      $checkpointId = $this->productionConfigDeployer->createCheckpoint($checkpointLabel);

      $this->output()->writeln(dt('<info>Created checkpoint: @id</info>', [
        '@id' => $checkpointId,
      ]));

      // Validate and apply the diff (handles temp storage cleanup).
      $results = $this->configDiff
        ->validateAndApplyDiff($diff);

      $this->output()->writeln(dt('<info>✓ Configuration diff applied successfully</info>'));
      $this->output()->writeln(dt('<info>Applied changes to @count configuration(s)</info>', [
        '@count' => count($results),
      ]));

      // Update last change timestamp.
      $this->configVerifier->updateLastChange($sourceLabel);
      return 0;

    }
    catch (\Exception $e) {
      $this->output()->writeln(dt('<error>Failed to apply diff: @message</error>', [
        '@message' => $e->getMessage(),
      ]));
      return 2;
    }
  }

  /**
   * Show current deployment status.
   */
  #[CLI\Command(name: 'config:preview-deploy:status', aliases: ['cpd:status'])]
  #[CLI\Help(description: 'Show current deployment status and configuration changes.')]
  public function status(): int {
    try {
      $baseVersion = $this->configDiff->getBaseVersion();

      if (!$baseVersion) {
        $this->output()->writeln(dt('<warning>No base checkpoint found. Run "drush cpd:init" first.</warning>'));
        return 1;
      }

      $this->output()->writeln(dt('<info>=== Config Preview Deploy Status ===</info>'));
      $this->output()->writeln(dt('Environment: @env', [
        '@env' => $this->getEnvironmentName(),
      ]));
      $this->output()->writeln(dt('Base version: @date', [
        '@date' => $this->dateFormatter->format($baseVersion['timestamp'], 'custom', 'Y-m-d H:i:s'),
      ]));
      $this->output()->writeln(dt('Base checkpoint: @id', [
        '@id' => $baseVersion['checkpoint_id'],
      ]));

      if ($this->configDiff->hasChanges()) {
        $changedConfigs = $this->configDiff->getChangedConfigs();
        $this->output()->writeln(dt('<info>Changes: @count configuration(s) modified</info>', [
          '@count' => count($changedConfigs),
        ]));

        if (count($changedConfigs) <= 10) {
          foreach ($changedConfigs as $config) {
            $this->output()->writeln(dt('- @config', ['@config' => $config]));
          }
        }
        else {
          $this->output()->writeln(dt('<comment>  (Too many to list - use "drush cpd:diff" for details)</comment>'));
        }
      }
      else {
        $this->output()->writeln(dt('<comment>Changes: None</comment>'));
      }

      // Production URL status.
      $config = $this->configFactory->get('config_preview_deploy.settings');
      $productionUrl = $config->get('production_url');

      if ($productionUrl) {
        $this->output()->writeln(dt('Production URL: @url', [
          '@url' => $productionUrl,
        ]));
      }
      else {
        $this->output()->writeln(dt('<warning>Production URL: Not configured</warning>'));
      }

    }
    catch (\Exception $e) {
      $this->output()->writeln(dt('<error>Failed to get status: @message</error>', [
        '@message' => $e->getMessage(),
      ]));
      return 2;
    }
    return 0;
  }

  /**
   * Register a preview environment for OAuth authorization.
   *
   * This command must be run on the PRODUCTION environment to register preview
   * environment URLs that are allowed to initiate OAuth authorization flows.
   * It automatically configures the OAuth consumer's redirect URI list.
   */
  #[CLI\Command(name: 'config:preview-deploy:register-environment', aliases: ['cpd:register'])]
  #[CLI\Help(description: 'Register a preview environment to allow OAuth authorization (run on PRODUCTION). Automatically adds the callback URI to the OAuth consumer redirect URI list.')]
  #[CLI\Argument(name: 'baseUri', description: 'Base URI of the preview environment (e.g., https://preview-123.example.com)')]
  #[CLI\Option(name: 'remove', description: 'Remove the environment instead of adding it.')]
  public function registerEnvironment(string $baseUri, array $options = ['remove' => FALSE]): int {
    try {
      // Validate base URI format.
      if (!filter_var($baseUri, FILTER_VALIDATE_URL)) {
        $this->output()->writeln(dt('<error>Invalid base URI format: @uri</error>', [
          '@uri' => $baseUri,
        ]));
        return 1;
      }

      // Construct the callback URI.
      $callbackUri = rtrim($baseUri, '/') . '/admin/config/development/config-preview-deploy/oauth/callback';

      // Load the OAuth consumer.
      $consumers = $this->entityTypeManager
        ->getStorage('consumer')
        ->loadByProperties(['client_id' => 'config_preview_deploy']);

      if (empty($consumers)) {
        $this->output()->writeln(dt('<error>OAuth consumer not found. Run module installation first.</error>'));
        return 1;
      }

      $consumer = reset($consumers);
      $currentRedirectUris = $consumer->get('redirect')->getValue();
      $redirectUris = array_column($currentRedirectUris, 'value');

      if ($options['remove']) {
        // Remove the URI.
        $index = array_search($callbackUri, $redirectUris);
        if ($index !== FALSE) {
          unset($redirectUris[$index]);
          $consumer->set('redirect', array_values($redirectUris));
          $consumer->save();

          $this->output()->writeln(dt('<info>✓ Removed preview environment: @uri</info>', [
            '@uri' => $baseUri,
          ]));
          $this->output()->writeln(dt('<comment>Callback URI removed: @callback</comment>', [
            '@callback' => $callbackUri,
          ]));
        }
        else {
          $this->output()->writeln(dt('<warning>Preview environment not found: @uri</warning>', [
            '@uri' => $baseUri,
          ]));
          return 1;
        }
      }
      else {
        // Add the URI if not already present.
        if (!in_array($callbackUri, $redirectUris)) {
          $redirectUris[] = $callbackUri;
          $consumer->set('redirect', $redirectUris);
          $consumer->save();

          $this->output()->writeln(dt('<info>✓ Registered preview environment: @uri</info>', [
            '@uri' => $baseUri,
          ]));
          $this->output()->writeln(dt('<comment>Callback URI added: @callback</comment>', [
            '@callback' => $callbackUri,
          ]));
        }
        else {
          $this->output()->writeln(dt('<comment>Preview environment already registered: @uri</comment>', [
            '@uri' => $baseUri,
          ]));
        }
      }

      // Show current registered environments.
      $this->output()->writeln(dt('<info>Currently registered environments:</info>'));
      if (empty($redirectUris)) {
        $this->output()->writeln(dt('<comment>  None</comment>'));
      }
      else {
        foreach ($redirectUris as $uri) {
          // Extract base URI from callback URI.
          $baseFromCallback = str_replace('/admin/config/development/config-preview-deploy/oauth/callback', '', $uri);
          $this->output()->writeln(dt('  - @base', ['@base' => $baseFromCallback]));
        }
      }

      return 0;

    }
    catch (\Exception $e) {
      $this->output()->writeln(dt('<error>Failed to register environment: @message</error>', [
        '@message' => $e->getMessage(),
      ]));
      return 2;
    }
  }

  /**
   * Gets the current environment name.
   */
  protected function getEnvironmentName(): string {
    return $this->configVerifier->getEnvironmentName();
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc