ai_upgrade_assistant-0.2.0-alpha2/src/Controller/UpgradeController.php
src/Controller/UpgradeController.php
<?php
namespace Drupal\ai_upgrade_assistant\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\State\StateInterface;
use Drupal\ai_upgrade_assistant\Service\ProjectAnalyzer;
use Drupal\ai_upgrade_assistant\Service\PatchSearcher;
use Drupal\ai_upgrade_assistant\Service\BatchAnalyzer;
use Drupal\ai_upgrade_assistant\Service\HuggingFaceService;
use Drupal\ai_upgrade_assistant\Service\AchievementService;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
/**
* Controller for handling upgrade operations.
*/
class UpgradeController extends ControllerBase {
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleList;
/**
* The project analyzer service.
*
* @var \Drupal\ai_upgrade_assistant\Service\ProjectAnalyzer
*/
protected $projectAnalyzer;
/**
* The patch searcher service.
*
* @var \Drupal\ai_upgrade_assistant\Service\PatchSearcher
*/
protected $patchSearcher;
/**
* The batch analyzer service.
*
* @var \Drupal\ai_upgrade_assistant\Service\BatchAnalyzer
*/
protected $batchAnalyzer;
/**
* The HuggingFace service.
*
* @var \Drupal\ai_upgrade_assistant\Service\HuggingFaceService
*/
protected $huggingFace;
/**
* The achievement service.
*
* @var \Drupal\ai_upgrade_assistant\Service\AchievementService
*/
protected $achievementService;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new UpgradeController object.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_list
* The module extension list.
* @param \Drupal\ai_upgrade_assistant\Service\ProjectAnalyzer $project_analyzer
* The project analyzer service.
* @param \Drupal\ai_upgrade_assistant\Service\PatchSearcher $patch_searcher
* The patch searcher service.
* @param \Drupal\ai_upgrade_assistant\Service\BatchAnalyzer $batch_analyzer
* The batch analyzer service.
* @param \Drupal\ai_upgrade_assistant\Service\HuggingFaceService $huggingFace
* The HuggingFace service.
* @param \Drupal\ai_upgrade_assistant\Service\AchievementService $achievement_service
* The achievement service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(
StateInterface $state,
ModuleHandlerInterface $module_handler,
ModuleExtensionList $module_list,
ProjectAnalyzer $project_analyzer,
PatchSearcher $patch_searcher,
BatchAnalyzer $batch_analyzer,
HuggingFaceService $huggingFace,
AchievementService $achievement_service,
RequestStack $request_stack
) {
$this->state = $state;
$this->moduleHandler = $module_handler;
$this->moduleList = $module_list;
$this->projectAnalyzer = $project_analyzer;
$this->patchSearcher = $patch_searcher;
$this->batchAnalyzer = $batch_analyzer;
$this->huggingFace = $huggingFace;
$this->achievementService = $achievement_service;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('state'),
$container->get('module_handler'),
$container->get('extension.list.module'),
$container->get('ai_upgrade_assistant.project_analyzer'),
$container->get('ai_upgrade_assistant.patch_searcher'),
$container->get('ai_upgrade_assistant.batch_analyzer'),
$container->get('ai_upgrade_assistant.huggingface'),
$container->get('ai_upgrade_assistant.achievement'),
$container->get('request_stack')
);
}
/**
* Check environment requirements.
*/
protected function checkEnvironment() {
$checks = [];
// Check PHP version
$php_version = phpversion();
$required_php = '8.1';
$checks[] = [
'title' => $this->t('PHP Version'),
'value' => $php_version,
'status' => version_compare($php_version, $required_php, '>='),
];
// Check Drupal core version
$drupal_version = \Drupal::VERSION;
$checks[] = [
'title' => $this->t('Drupal Core Version'),
'value' => $drupal_version,
'status' => version_compare($drupal_version, '10.0.0', '<'),
];
// Check HuggingFace API key
$api_key = $this->state->get('ai_upgrade_assistant.huggingface_api_key');
$checks[] = [
'title' => $this->t('HuggingFace API Key'),
'value' => $api_key ? $this->t('Configured') : $this->t('Not configured'),
'status' => !empty($api_key),
];
// Check write permissions
$module_path = $this->moduleHandler->getModule('ai_upgrade_assistant')->getPath();
$is_writable = is_writable($module_path);
$checks[] = [
'title' => $this->t('Module Directory Permissions'),
'value' => $is_writable ? $this->t('Writable') : $this->t('Not writable'),
'status' => $is_writable,
];
return $checks;
}
/**
* Get modules grouped by status.
*
* @return array
* An array of modules grouped by status.
*/
protected function getModulesByStatus() {
$modules = [
'compatible' => [],
'needs_update' => [],
'unknown' => [],
];
$installed_modules = $this->moduleHandler->getModuleList();
foreach ($installed_modules as $name => $extension) {
// Get module info using the proper method
$module_info = $this->moduleList->getExtensionInfo($name);
if ($this->isModuleCompatible($name)) {
$modules['compatible'][] = [
'name' => [
'#markup' => $module_info['name'],
],
'machine_name' => [
'#markup' => $name,
],
'version' => [
'#markup' => $module_info['version'] ?? 'Unknown',
],
'status' => [
'#markup' => 'compatible',
],
];
}
else {
$modules['needs_update'][] = [
'name' => [
'#markup' => $module_info['name'],
],
'machine_name' => [
'#markup' => $name,
],
'version' => [
'#markup' => $module_info['version'] ?? 'Unknown',
],
'status' => [
'#markup' => 'needs_update',
],
];
}
}
return $modules;
}
/**
* Calculate overall progress.
*
* @return array
* Progress information.
*/
protected function calculateProgress() {
$modules = $this->getModulesByStatus();
$total_modules = 0;
$compatible_modules = 0;
foreach ($modules as $status => $status_modules) {
$total_modules += count($status_modules);
if ($status === 'compatible') {
$compatible_modules += count($status_modules);
}
}
$percentage = $total_modules > 0
? round(($compatible_modules / $total_modules) * 100)
: 0;
return [
'total' => $total_modules,
'compatible' => $compatible_modules,
'percentage' => $percentage,
];
}
/**
* Automatically fixes all modules that can be automatically upgraded.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response object.
*/
public function autoFixAll() {
$modules = $this->getModulesByStatus();
$batch = [
'title' => $this->t('Upgrading modules...'),
'operations' => [],
'finished' => [$this, 'batchFinished'],
'progress_message' => $this->t('Processed @current out of @total modules.'),
'error_message' => $this->t('An error occurred during the upgrade process.'),
];
foreach ($modules['needs_update'] as $module) {
if ($module['has_fix']) {
$batch['operations'][] = [
[$this, 'batchProcessModule'],
[$module],
];
}
}
batch_set($batch);
return $this->redirect('ai_upgrade_assistant.status');
}
/**
* Auto-fix a specific module.
*
* @param string $module_name
* The name of the module to auto-fix.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
* JSON response with the status of the operation or a redirect response.
*/
public function autoFixModule($module_name) {
try {
if (empty($module_name)) {
throw new \InvalidArgumentException('Module name is required.');
}
if (!$this->moduleHandler->moduleExists($module_name)) {
return new JsonResponse([
'status' => 'error',
'message' => $this->t('Module @module does not exist.', ['@module' => $module_name]),
], 404);
}
// Get module path
$module_path = $this->moduleHandler->getModule($module_name)->getPath();
// Create batch operation
$batch = [
'title' => $this->t('Auto-fixing @module', ['@module' => $module_name]),
'operations' => [
[
[$this->batchAnalyzer, 'analyzeModule'],
[$module_name, $module_path],
],
[
[$this->projectAnalyzer, 'applyFixes'],
[$module_name],
],
],
'finished' => [$this->batchAnalyzer, 'finishBatch'],
'file' => drupal_get_path('module', 'ai_upgrade_assistant') . '/src/Service/BatchAnalyzer.php',
'progressive' => TRUE,
];
batch_set($batch);
// For AJAX requests, return JSON
if ($this->requestStack->getCurrentRequest()->isXmlHttpRequest()) {
return new JsonResponse([
'status' => 'success',
'message' => $this->t('Started auto-fix process for @module.', ['@module' => $module_name]),
]);
}
// For non-AJAX requests, redirect to status page
return $this->redirect('ai_upgrade_assistant.status');
}
catch (\Exception $e) {
$this->getLogger('ai_upgrade_assistant')->error(
'Error auto-fixing module @module: @error',
[
'@module' => $module_name ?? 'unknown',
'@error' => $e->getMessage(),
]
);
if ($this->requestStack->getCurrentRequest()->isXmlHttpRequest()) {
return new JsonResponse([
'status' => 'error',
'message' => $this->t('Error auto-fixing module: @error', ['@error' => $e->getMessage()]),
], 500);
}
$this->messenger()->addError($this->t('Error auto-fixing module: @error', [
'@error' => $e->getMessage(),
]));
return $this->redirect('ai_upgrade_assistant.status');
}
}
/**
* Process a single module in the batch operation.
*
* @param array $module
* The module to process.
* @param array &$context
* The batch context array.
*/
public function batchProcessModule($module, &$context) {
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_module'] = 0;
$context['sandbox']['max'] = 1;
}
try {
// Analyze module
$analysis = $this->projectAnalyzer->analyzeModule($module['name']);
// Search for patches
$patches = $this->patchSearcher->findPatches($module['name'], $analysis);
// Process module with batch analyzer
$this->batchAnalyzer->processModule($module['name'], $analysis, $patches);
$context['message'] = $this->t('Upgraded @name module.', ['@name' => $module['name']]);
$context['results'][] = $module['name'];
}
catch (\Exception $e) {
$context['results']['errors'][] = $this->t('Error upgrading @name: @error', [
'@name' => $module['name'],
'@error' => $e->getMessage(),
]);
}
$context['sandbox']['progress']++;
$context['sandbox']['current_module']++;
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
/**
* Finish batch processing.
*
* @param bool $success
* Whether the batch completed successfully.
* @param array $results
* The batch results array.
* @param array $operations
* The batch operations array.
*/
public function batchFinished($success, $results, $operations) {
if ($success) {
if (!empty($results['errors'])) {
foreach ($results['errors'] as $error) {
$this->messenger()->addError($error);
}
}
else {
$count = count($results);
$this->messenger()->addStatus($this->formatPlural(
$count,
'Successfully upgraded 1 module.',
'Successfully upgraded @count modules.'
));
}
}
else {
$this->messenger()->addError($this->t('An error occurred while upgrading the modules.'));
}
$this->completeUpgrade($success);
}
/**
* Displays the upgrade status page.
*
* @return array
* A render array for the status page.
*/
public function status() {
// Get environment checks
$environment = $this->checkEnvironment();
// Get modules by status
$modules_by_status = $this->getModulesByStatus();
// Calculate progress
$progress = $this->calculateProgress();
// Format the data for rendering
$build = [
'#theme' => 'ai_upgrade_assistant_status',
'#environment' => [
'items' => array_map(function($check) {
return [
'status' => [
'#markup' => $check['status'] ? 'success' : 'error',
],
'message' => [
'#markup' => $this->t('@title: @value', [
'@title' => $check['title'],
'@value' => $check['value'],
]),
],
];
}, $environment),
],
'#progress' => [
'data' => [
'#markup' => $progress['percentage'],
],
],
'#modules' => [
'groups' => [],
],
'#attached' => [
'library' => [
'ai_upgrade_assistant/status_page',
],
],
];
// Group modules by status
foreach ($modules_by_status as $status => $modules) {
$build['#modules']['groups'][$status] = $modules;
}
return $build;
}
/**
* Checks the current upgrade status.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* JSON response containing the current upgrade status.
*/
public function checkStatus() {
$response = [
'status' => 'in_progress',
'message' => '',
'progress' => 0,
'errors' => [],
'terminal_output' => [],
];
try {
// Get stored state
$state = $this->state->get('ai_upgrade_assistant.upgrade_status', []);
if (!empty($state)) {
$response = array_merge($response, $state);
}
// Calculate current progress
$progress = $this->calculateProgress();
$response['progress'] = $progress['percentage'];
// Get modules by status
$modules = $this->getModulesByStatus();
$response['modules'] = [
'compatible' => count($modules['compatible']),
'needs_update' => count($modules['needs_update']),
];
// Check if process is complete
if ($progress['percentage'] === 100) {
$response['status'] = 'complete';
$response['message'] = $this->t('Upgrade process completed successfully.');
}
// Get terminal output
$terminal_output = $this->state->get('ai_upgrade_assistant.terminal_output', []);
if (!empty($terminal_output)) {
$response['terminal_output'] = $terminal_output;
}
}
catch (\Exception $e) {
$response['status'] = 'error';
$response['message'] = $this->t('Error checking upgrade status: @error', ['@error' => $e->getMessage()]);
$response['errors'][] = $e->getMessage();
}
return new JsonResponse($response);
}
/**
* Initialize the terminal output with default messages.
*/
protected function initializeTerminalOutput() {
$this->state->set('ai_upgrade_assistant.terminal_output', [
[
'message' => $this->t('Initializing AI Upgrade Assistant...'),
'type' => 'info',
'timestamp' => time(),
],
]);
}
/**
* Add a message to the terminal output.
*
* @param string $message
* The message to add.
* @param string $type
* The message type (info, success, warning, error).
*
* @return array
* The updated terminal output array.
*/
protected function addTerminalMessage($message, $type = 'info') {
$terminal_output = $this->state->get('ai_upgrade_assistant.terminal_output', []);
$terminal_output[] = [
'message' => $message,
'type' => $type,
'timestamp' => time(),
];
$this->state->set('ai_upgrade_assistant.terminal_output', $terminal_output);
return $terminal_output;
}
/**
* Get the installed Composer version.
*
* @return string
* The Composer version string.
*/
protected function getComposerVersion() {
try {
$process = new Process(['composer', '--version']);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
$output = $process->getOutput();
preg_match('/Composer version ([0-9.]+)/', $output, $matches);
return $matches[1] ?? '0.0.0';
}
catch (\Exception $e) {
return '0.0.0';
}
}
/**
* Convert PHP memory limit string to bytes.
*
* @param string $memory_limit
* Memory limit string (e.g., '128M', '1G').
*
* @return int
* Memory limit in bytes.
*/
protected function convertToBytes($memory_limit) {
$value = (int) $memory_limit;
switch (strtolower(substr($memory_limit, -1))) {
case 'g':
$value *= 1024;
case 'm':
$value *= 1024;
case 'k':
$value *= 1024;
}
return $value;
}
/**
* Check if a module is compatible with Drupal 11.
*
* @param string $name
* The module name.
*
* @return bool
* TRUE if the module is compatible, FALSE otherwise.
*/
protected function isModuleCompatible($name) {
// For now, just return FALSE to indicate all modules need fixing
return FALSE;
}
/**
* Check if a module has a known fix available.
*
* @param string $name
* The module name.
*
* @return bool
* TRUE if a fix is available, FALSE otherwise.
*/
protected function hasKnownFix($name) {
// For now, return TRUE for testing
return TRUE;
}
/**
* Handles the completion of an upgrade process.
*
* @param bool $success
* Whether the upgrade was successful.
* @param int $complexity
* The complexity of the upgrade.
*/
protected function completeUpgrade($success, $complexity = 3) {
$account = $this->currentUser();
// Award points based on success and complexity
$this->achievementService->awardUpgradePoints(
$account->id(),
$success,
$complexity
);
// If successful, check for pattern contribution
if ($success) {
$pattern_quality = min(5, ceil($complexity * 1.5));
$this->achievementService->awardPatternPoints(
$account->id(),
$pattern_quality
);
}
}
/**
* Gets the current rate limit status.
*
* @return array
* Render array for the rate limit status page.
*/
public function getRateLimitStatus() {
$rate_limits = $this->huggingFace->getRateLimits();
return [
'#theme' => 'rate_limit_status',
'#rate_limits' => [
'current_usage' => [
'#markup' => $rate_limits['current_usage'] ?? 0,
],
'limit' => [
'#markup' => $rate_limits['limit'] ?? 'Unknown',
],
'reset_time' => [
'#markup' => $rate_limits['reset_time']
? date('Y-m-d H:i:s', $rate_limits['reset_time'])
: $this->t('Unknown'),
],
],
'#cache' => [
'max-age' => 60, // Cache for 1 minute
],
];
}
}
