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();
}
}
