ai_upgrade_assistant-0.2.0-alpha2/src/Service/PatchSearcher.php

src/Service/PatchSearcher.php
<?php

namespace Drupal\ai_upgrade_assistant\Service;

use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\update\UpdateManagerInterface;
use GuzzleHttp\ClientInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;

/**
 * Service for finding and managing patches for module upgrades.
 */
class PatchSearcher {
  use StringTranslationTrait;
  use DependencySerializationTrait;

  /**
   * The update manager.
   *
   * @var \Drupal\update\UpdateManagerInterface
   */
  protected $updateManager;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The HTTP client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The logger factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * The cache backend.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * Constructs a new PatchSearcher.
   *
   * @param \Drupal\update\UpdateManagerInterface $update_manager
   *   The update manager service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The HTTP client.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend.
   */
  public function __construct(
    UpdateManagerInterface $update_manager,
    ModuleHandlerInterface $module_handler,
    ClientInterface $http_client,
    FileSystemInterface $file_system,
    LoggerChannelFactoryInterface $logger_factory,
    StateInterface $state,
    CacheBackendInterface $cache
  ) {
    $this->updateManager = $update_manager;
    $this->moduleHandler = $module_handler;
    $this->httpClient = $http_client;
    $this->fileSystem = $file_system;
    $this->loggerFactory = $logger_factory;
    $this->state = $state;
    $this->cache = $cache;
  }

  /**
   * Check module compatibility with a target Drupal version.
   *
   * @param string $module_name
   *   The module name.
   * @param string $current_version
   *   Current version of the module.
   * @param string $target_version
   *   Target Drupal core version.
   *
   * @return array
   *   Compatibility information array containing:
   *   - compatible_version: The compatible version if found
   *   - update_type: Type of update (major, minor, none)
   *   - message: Status message
   *   - core_compatibility: Core version compatibility
   */
  public function checkModuleCompatibility($module_name, $current_version, $target_version) {
    // Refresh available updates data
    if (!$this->state->get('update.last_check') || 
        $this->state->get('update.last_check') < (REQUEST_TIME - (24 * 60 * 60))) {
      $this->updateManager->refreshUpdateData();
    }

    $project_data = $this->updateManager->getProjects();
    if (!isset($project_data[$module_name])) {
      $this->loggerFactory->get('ai_upgrade_assistant')
        ->notice('No update information available for @module', ['@module' => $module_name]);
      return [
        'compatible_version' => NULL,
        'update_type' => 'none',
        'message' => $this->t('No update information available'),
        'core_compatibility' => NULL,
      ];
    }

    $project = $project_data[$module_name];
    $compatible_release = NULL;
    $update_type = 'none';

    // Check each available release for compatibility
    if (!empty($project['releases'])) {
      foreach ($project['releases'] as $version => $release) {
        // Skip if no core compatibility info
        if (empty($release['core_compatibility'])) {
          continue;
        }

        // Check if release is compatible with target version
        if (version_compare($release['core_compatibility'], $target_version, '>=')) {
          // Determine update type
          $current_major = explode('.', $current_version)[0];
          $release_major = explode('.', $version)[0];
          
          if ($current_major != $release_major) {
            $update_type = 'major';
          } elseif (version_compare($version, $current_version, '>')) {
            $update_type = 'minor';
          }

          $compatible_release = $release;
          break;
        }
      }
    }

    if ($compatible_release) {
      return [
        'compatible_version' => $compatible_release['version'],
        'update_type' => $update_type,
        'message' => $this->t('Compatible version found'),
        'core_compatibility' => $compatible_release['core_compatibility'],
      ];
    }

    return [
      'compatible_version' => NULL,
      'update_type' => 'none',
      'message' => $this->t('No compatible version found for Drupal @version', ['@version' => $target_version]),
      'core_compatibility' => NULL,
    ];
  }

  /**
   * Find existing patches for a module.
   *
   * @param string $module_name
   *   The module name.
   * @param string $issue_description
   *   Description to search for.
   * @param array $options
   *   Additional options for the search.
   *
   * @return array
   *   Array of found patches.
   */
  public function findExistingPatches($module_name, $issue_description, array $options = []) {
    $this->updateTerminalOutput("Searching for patches for $module_name...");
    $patches = [];
    
    // Get project data from update manager
    $project_data = $this->updateManager->getProjects();
    if (!isset($project_data[$module_name])) {
      $this->updateTerminalOutput("No update information available for $module_name");
      return [];
    }

    $project = $project_data[$module_name];

    // First check release notes for D11 compatibility mentions
    if (!empty($project['releases'])) {
      foreach ($project['releases'] as $version => $release) {
        if (!empty($release['release_notes']) && 
            stripos($release['release_notes'], 'drupal 11') !== FALSE) {
          $patches[] = [
            'title' => $this->t('Version @version release notes', ['@version' => $version]),
            'url' => $release['release_link'],
            'status' => 'released',
            'created' => $release['date'],
            'changed' => $release['date'],
            'type' => 'release',
          ];
        }
      }
    }

    // Check if we have cached issue data
    $cid = 'ai_upgrade_assistant:issues:' . $module_name;
    $cache = $this->cache->get($cid);
    
    if ($cache && $cache->data) {
      $this->updateTerminalOutput("Using cached issue data for $module_name");
      return array_merge($patches, $cache->data);
    }

    // Then try to search issue queue if available
    try {
      // Use the REST API v2 endpoint
      $api_url = sprintf(
        'https://www.drupal.org/api/project/%s/issues?filter[category][value][]=bug&filter[category][value][]=task&filter[status][value][]=1&filter[status][value][]=8&filter[status][value][]=13&filter[status][value][]=14&sort=-changed',
        urlencode($module_name)
      );

      $request_options = [
        'headers' => [
          'Accept' => 'application/vnd.api+json',
        ],
        'timeout' => 5,
        'connect_timeout' => 3,
      ];

      $response = $this->httpClient->get($api_url, $request_options);
      $status_code = $response->getStatusCode();

      if ($status_code === 200) {
        $data = json_decode($response->getBody(), TRUE);
        $issue_patches = [];

        if (!empty($data['data'])) {
          foreach ($data['data'] as $issue) {
            $attributes = $issue['attributes'];
            // Check if issue is related to D11 compatibility
            if (stripos($attributes['title'], 'drupal 11') !== FALSE || 
                stripos($attributes['body'], 'drupal 11') !== FALSE) {
              $issue_patches[] = [
                'title' => $attributes['title'],
                'url' => sprintf('https://www.drupal.org/node/%s', $issue['id']),
                'status' => $attributes['status'],
                'created' => $attributes['created'],
                'changed' => $attributes['changed'],
                'type' => 'issue',
              ];
            }
          }

          // Cache the issue data for 1 hour
          $this->cache->set($cid, $issue_patches, time() + 3600);
          $this->updateTerminalOutput("Found " . count($issue_patches) . " patches for $module_name");
          $patches = array_merge($patches, $issue_patches);
        }
      } else {
        // Try to use cached data if available
        $cache = $this->cache->get($cid);
        if ($cache && $cache->data) {
          $this->updateTerminalOutput("Using cached issue data for $module_name");
          $patches = array_merge($patches, $cache->data);
        }

        $this->loggerFactory->get('ai_upgrade_assistant')
          ->warning('Drupal.org API returned status code @code for @module. Using cached data if available.', [
            '@code' => $status_code,
            '@module' => $module_name,
          ]);
      }
    }
    catch (\Exception $e) {
      // Try to use cached data if available
      $cache = $this->cache->get($cid);
      if ($cache && $cache->data) {
        $this->updateTerminalOutput("Using cached issue data for $module_name");
        $patches = array_merge($patches, $cache->data);
      }

      $this->loggerFactory->get('ai_upgrade_assistant')
        ->warning('Error searching Drupal.org issues for @module: @error. Using cached data if available.', [
          '@module' => $module_name,
          '@error' => $e->getMessage(),
        ]);
    }

    return $patches;
  }

  /**
   * Find patches for a module.
   *
   * @param string $module_name
   *   The module name.
   * @param array $context
   *   Additional context for patch search.
   *
   * @return array
   *   Array of found patches with:
   *   - url: URL to the patch
   *   - description: Description of what the patch does
   *   - status: Status of the patch (e.g., 'needs review', 'reviewed')
   *   - created: Timestamp when patch was created
   *   - changed: Timestamp when patch was last updated
   */
  public function findPatches($module_name, array $context = []) {
    $cid = 'ai_upgrade_assistant:patches:' . $module_name;
    if ($cached = $this->cache->get($cid)) {
      return $cached->data;
    }

    $patches = [];
    try {
      // Search for existing patches
      $existing_patches = $this->findExistingPatches(
        $module_name,
        $context['issue_description'] ?? 'Drupal 11 compatibility'
      );
      $patches = array_merge($patches, $existing_patches);

      // Check module compatibility
      $module = $this->moduleHandler->getModule($module_name);
      if ($module && !empty($module->info['version'])) {
        $compatibility = $this->checkModuleCompatibility(
          $module_name,
          $module->info['version'],
          $context['target_version'] ?? '11.0'
        );

        if ($compatibility['update_type'] !== 'none') {
          $patches[] = [
            'type' => 'version_update',
            'description' => $this->t('Update to version @version for Drupal @core compatibility', [
              '@version' => $compatibility['compatible_version'],
              '@core' => $compatibility['core_compatibility'],
            ]),
            'version' => $compatibility['compatible_version'],
            'core_compatibility' => $compatibility['core_compatibility'],
            'update_type' => $compatibility['update_type'],
          ];
        }
      }

      // Cache for 1 hour
      $this->cache->set($cid, $patches, time() + 3600);

      return $patches;
    }
    catch (\Exception $e) {
      $this->loggerFactory->get('ai_upgrade_assistant')->error(
        'Error finding patches for @module: @error',
        [
          '@module' => $module_name,
          '@error' => $e->getMessage(),
        ]
      );
      return [];
    }
  }

  /**
   * Get the composer command to update a module.
   *
   * @param array $compatibility
   *   Compatibility information from checkModuleCompatibility().
   *
   * @return string|null
   *   Composer command or NULL if not applicable.
   */
  public function getComposerUpdateCommand($compatibility) {
    if (empty($compatibility['compatible_version'])) {
      return NULL;
    }

    $constraint = $compatibility['compatible_version'];
    if ($compatibility['update_type'] === 'minor') {
      // For minor updates, stay within the same major version
      $major_version = explode('.', $compatibility['compatible_version'])[0];
      $constraint = "^{$major_version}.0";
    }

    return sprintf(
      'composer require drupal/%s:%s --update-with-dependencies',
      $compatibility['module'],
      $constraint
    );
  }

  /**
   * Updates the terminal output in the state system.
   *
   * @param string $message
   *   The message to append to the terminal output.
   */
  protected function updateTerminalOutput($message) {
    $output = $this->state->get('ai_upgrade_assistant.terminal_output', []);
    $output[] = [
      'timestamp' => time(),
      'message' => $message,
    ];
    // Keep only the last 100 messages
    if (count($output) > 100) {
      $output = array_slice($output, -100);
    }
    $this->state->set('ai_upgrade_assistant.terminal_output', $output);
  }

}

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

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