- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
241 lines
7.9 KiB
PHP
241 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Console;
|
|
|
|
/**
|
|
* Executes command workflows
|
|
*/
|
|
final readonly class WorkflowExecutor
|
|
{
|
|
public function __construct(
|
|
private CommandRegistry $commandRegistry,
|
|
private CommandGroupRegistry $groupRegistry,
|
|
private ConsoleOutputInterface $output
|
|
) {}
|
|
|
|
/**
|
|
* Execute workflow by name
|
|
*/
|
|
public function executeWorkflow(string $workflowName, array $context = []): WorkflowResult
|
|
{
|
|
$workflow = $this->groupRegistry->getWorkflow($workflowName);
|
|
if ($workflow === null) {
|
|
return WorkflowResult::error("Workflow '{$workflowName}' not found");
|
|
}
|
|
|
|
return $this->executeWorkflowSteps($workflow, $context);
|
|
}
|
|
|
|
/**
|
|
* Execute workflow steps
|
|
*/
|
|
public function executeWorkflowSteps(array $workflow, array $context = []): WorkflowResult
|
|
{
|
|
$this->output->writeLine("🔄 Starting workflow: {$workflow['name']}", ConsoleColor::BRIGHT_CYAN);
|
|
|
|
if (!empty($workflow['description'])) {
|
|
$this->output->writeLine($workflow['description'], ConsoleColor::GRAY);
|
|
}
|
|
|
|
$this->output->newLine();
|
|
|
|
// Check prerequisites
|
|
if (!$this->checkPrerequisites($workflow['prerequisites'], $context)) {
|
|
return WorkflowResult::error('Prerequisites not met');
|
|
}
|
|
|
|
$results = [];
|
|
$stepNumber = 1;
|
|
$totalSteps = count($workflow['steps']);
|
|
|
|
foreach ($workflow['steps'] as $step) {
|
|
$workflowStep = $step instanceof WorkflowStep ? $step : WorkflowStep::fromArray($step);
|
|
|
|
$this->output->writeLine(
|
|
"Step {$stepNumber}/{$totalSteps}: {$workflowStep->getDescription() ?: $workflowStep->getCommand()}",
|
|
ConsoleColor::BRIGHT_WHITE
|
|
);
|
|
|
|
// Check if step should be executed
|
|
if (!$workflowStep->shouldExecute($context)) {
|
|
$this->output->writeLine(' ⏭️ Skipped (condition not met)', ConsoleColor::YELLOW);
|
|
$stepNumber++;
|
|
continue;
|
|
}
|
|
|
|
$stepResult = $this->executeStep($workflowStep, $context);
|
|
$results[] = $stepResult;
|
|
|
|
if ($stepResult->isSuccess()) {
|
|
$this->output->writeLine(' ✅ Success', ConsoleColor::GREEN);
|
|
} else {
|
|
$message = " ❌ Failed: {$stepResult->getError()}";
|
|
|
|
if ($workflowStep->isOptional()) {
|
|
$this->output->writeLine("{$message} (optional step)", ConsoleColor::YELLOW);
|
|
} else {
|
|
$this->output->writeLine($message, ConsoleColor::RED);
|
|
|
|
if ($workflow['stopOnError']) {
|
|
$this->output->newLine();
|
|
$this->output->writeLine('🛑 Workflow stopped due to error', ConsoleColor::RED);
|
|
|
|
// Execute rollback if configured
|
|
if (!empty($workflow['rollbackSteps'])) {
|
|
$this->executeRollback($workflow['rollbackSteps'], $context);
|
|
}
|
|
|
|
return WorkflowResult::error("Step failed: {$stepResult->getError()}");
|
|
}
|
|
}
|
|
}
|
|
|
|
$stepNumber++;
|
|
}
|
|
|
|
$this->output->newLine();
|
|
$this->output->writeLine('🎉 Workflow completed successfully!', ConsoleColor::BRIGHT_GREEN);
|
|
|
|
return WorkflowResult::success($results);
|
|
}
|
|
|
|
/**
|
|
* Execute a single workflow step
|
|
*/
|
|
private function executeStep(WorkflowStep $step, array $context): StepResult
|
|
{
|
|
$retryCount = 0;
|
|
$maxRetries = $step->getRetryCount();
|
|
|
|
do {
|
|
try {
|
|
// Merge step environment with context
|
|
$stepContext = array_merge($context, $step->getEnvironment());
|
|
|
|
// Build command arguments
|
|
$args = ['console', $step->getCommand()];
|
|
foreach ($step->getParameters() as $param) {
|
|
// Simple parameter substitution
|
|
$args[] = $this->substituteVariables($param, $stepContext);
|
|
}
|
|
|
|
$exitCode = $this->commandRegistry->execute($args);
|
|
|
|
if ($exitCode->value === 0) {
|
|
return StepResult::success($step->getCommand());
|
|
} else {
|
|
$error = "Command exited with code {$exitCode->value}";
|
|
|
|
if ($retryCount < $maxRetries) {
|
|
$retryCount++;
|
|
$this->output->writeLine(
|
|
" ⏳ Retrying ({$retryCount}/{$maxRetries})...",
|
|
ConsoleColor::YELLOW
|
|
);
|
|
continue;
|
|
}
|
|
|
|
return StepResult::error($error);
|
|
}
|
|
} catch (\Exception $e) {
|
|
$error = "Exception: {$e->getMessage()}";
|
|
|
|
if ($retryCount < $maxRetries) {
|
|
$retryCount++;
|
|
$this->output->writeLine(
|
|
" ⏳ Retrying ({$retryCount}/{$maxRetries})...",
|
|
ConsoleColor::YELLOW
|
|
);
|
|
continue;
|
|
}
|
|
|
|
return StepResult::error($error);
|
|
}
|
|
} while ($retryCount <= $maxRetries);
|
|
|
|
return StepResult::error('Max retries exceeded');
|
|
}
|
|
|
|
/**
|
|
* Execute rollback steps
|
|
*/
|
|
private function executeRollback(array $rollbackSteps, array $context): void
|
|
{
|
|
$this->output->writeLine('🔄 Executing rollback...', ConsoleColor::YELLOW);
|
|
|
|
foreach ($rollbackSteps as $step) {
|
|
$workflowStep = $step instanceof WorkflowStep ? $step : WorkflowStep::fromArray($step);
|
|
$this->output->writeLine(" Rolling back: {$workflowStep->getCommand()}", ConsoleColor::GRAY);
|
|
|
|
$result = $this->executeStep($workflowStep, $context);
|
|
if ($result->isSuccess()) {
|
|
$this->output->writeLine(' ✅ Rollback success', ConsoleColor::GREEN);
|
|
} else {
|
|
$this->output->writeLine(' ❌ Rollback failed', ConsoleColor::RED);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check workflow prerequisites
|
|
*/
|
|
private function checkPrerequisites(array $prerequisites, array $context): bool
|
|
{
|
|
if (empty($prerequisites)) {
|
|
return true;
|
|
}
|
|
|
|
$this->output->writeLine('🔍 Checking prerequisites...', ConsoleColor::YELLOW);
|
|
|
|
foreach ($prerequisites as $prerequisite) {
|
|
// Simple prerequisite checking - can be extended
|
|
if ($prerequisite === 'database') {
|
|
$result = $this->checkDatabaseConnection();
|
|
} elseif ($prerequisite === 'migrations') {
|
|
$result = $this->checkMigrations();
|
|
} else {
|
|
$result = true;
|
|
}
|
|
|
|
if (!$result) {
|
|
$this->output->writeLine("❌ Prerequisite failed: {$prerequisite}", ConsoleColor::RED);
|
|
return false;
|
|
}
|
|
|
|
$this->output->writeLine("✅ {$prerequisite}", ConsoleColor::GREEN);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Substitute variables in parameters
|
|
*/
|
|
private function substituteVariables(string $value, array $context): string
|
|
{
|
|
// Simple variable substitution: {{variable}}
|
|
return preg_replace_callback('/\{\{(\w+)\}\}/', function($matches) use ($context) {
|
|
return $context[$matches[1]] ?? $matches[0];
|
|
}, $value);
|
|
}
|
|
|
|
/**
|
|
* Check database connection (placeholder)
|
|
*/
|
|
private function checkDatabaseConnection(): bool
|
|
{
|
|
// Implementation would check actual database connection
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check migrations (placeholder)
|
|
*/
|
|
private function checkMigrations(): bool
|
|
{
|
|
// Implementation would check migration status
|
|
return true;
|
|
}
|
|
} |