drupalorg-1.0.x-dev/src/ProjectService.php

src/ProjectService.php
<?php

namespace Drupal\drupalorg;

use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Psr\Log\LoggerInterface;

/**
 * Project related helper methods.
 */
class ProjectService {

  use StringTranslationTrait;

  /**
   * Types of projects.
   *
   * This comes from "project_project_node_types" result in D7.
   *
   * @var array
   */
  const PROJECT_TYPES = [
    'project_module',
    'project_theme',
    'project_distribution',
    'project_theme_engine',
    'project_general',
    'project_drupalorg',
    'project_translation',
    'project_core',
  ];

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected Connection $connection;

  /**
   * Construct method.
   *
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   */
  public function __construct(Connection $connection, protected EntityTypeManagerInterface $entityTypeManager, protected LoggerInterface $logger) {
    $this->connection = $connection;
  }

  /**
   * Retrieve a project given a value and a field.
   *
   * @param string $field
   *   Field to query.
   * @param string $value
   *   Value to check.
   * @param string|null $type
   *   Type of project node to query, otherwise all project types.
   *
   * @return \Drupal\node\NodeInterface|null
   *   Project node or null.
   */
  protected function getProjectBy(string $field, string $value, ?string $type = NULL): ?NodeInterface {
    $project_types = is_null($type) ? self::PROJECT_TYPES : [$type];
    $project_id = $this->entityTypeManager->getStorage('node')->getQuery()
      ->accessCheck(FALSE)
      ->condition('type', $project_types, 'IN')
      ->condition($field, $value)
      ->range(0, 1)
      ->execute();
    if (!empty($project_id)) {
      return $this->entityTypeManager->getStorage('node')->load(reset($project_id));
    }

    return NULL;
  }

  /**
   * Retrieve a project from its composer username.
   *
   * @param string $composer_namespace
   *   Composer namespace of the project.
   *
   * @return \Drupal\node\NodeInterface|null
   *   Project node or null.
   */
  public function getProjectByComposerNamespace(string $composer_namespace): ?NodeInterface {
    return $this->getProjectBy('field_composer_namespace', $composer_namespace, 'project_module');
  }

  /**
   * Retrieve a project from its machine name.
   *
   * @param string $machine_name
   *   Machine name of the project.
   *
   * @return \Drupal\node\NodeInterface|null
   *   Project node or null.
   */
  public function getProjectByMachineName(string $machine_name): ?NodeInterface {
    return $this->getProjectBy('field_project_machine_name', $machine_name);
  }

  /**
   * Generates a link to a project by the given machine name.
   *
   * @param string $text
   *   Machine name of the project.
   *
   * @return string
   *   Original text or link to the project if found.
   */
  public function getProjectLinkFromMachineName($text) {
    $project_name = $text ?? '';
    $project_name = strip_tags($project_name);
    $project_name = str_replace(["\r", "\n", ' '], '', $project_name);
    $project = $this->getProjectByMachineName($project_name);
    if ($project) {
      $text = $project->toLink()->toString();
      // Sandbox projects.
      if (is_numeric($project_name)) {
        $text .= ' <small>(' . $project_name . ')</small>';
      }
    }
    else {
      $text = $project_name ?: $this->t('<none>');
    }

    return $text;
  }

  /**
   * Get the maintainers of a given project.
   *
   * @param \Drupal\node\NodeInterface $project
   *   Project to get maintainers from.
   *
   * @return array
   *   Array of users that are maintainers of the project and their levels.
   */
  public function getProjectMaintainers(NodeInterface $project): array {
    if (!$this->isProject($project)) {
      throw new \Exception('The entity is not a project.');
    }

    $query = $this->connection
      ->select('drupalorg_project_maintainer', 'pm')
      ->condition('pm.nid', $project->id())
      ->fields('pm');
    return $query->execute()->fetchAllAssoc('uid');
  }

  /**
   * Return whether a given user is a maintainer of a project node.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   User to check.
   * @param \Drupal\node\NodeInterface $node
   *   Node to check.
   * @param array $levels_to_check
   *   (Optional) Levels to check. If NULL, all levels will be checked.
   *
   * @return int
   *   Added levels of maintainer properties.
   */
  public function isProjectMaintainer(AccountInterface $account, NodeInterface $node, ?array $levels_to_check = NULL) {
    $added_levels = 0;
    $maintainers = $this->getProjectMaintainers($node);
    $user_id = $account->id();
    if (!empty($maintainers[$user_id])) {
      $levels = $maintainers[$user_id];
      if (is_null($levels_to_check)) {
        $levels_to_check = [
          'update_project',
          'administer_maintainers',
          'write_to_vcs',
          'manage_releases',
          'maintain_issues',
        ];
      }
      foreach ($levels_to_check as $level) {
        if (isset($levels->{$level})) {
          $added_levels += (int) $levels->{$level};
        }
      }
    }

    return $added_levels;
  }

  /**
   * Get the repository information of a given project.
   *
   * @param \Drupal\node\NodeInterface $project
   *   Project to get repository information from.
   *
   * @return array
   *   Repository namespace, name and id.
   */
  public function getProjectRepositoryInformation(NodeInterface $project): array {
    $query = $this->connection
      ->select('drupalorg_project_repositories', 'pr')
      ->condition('pr.drupal_project_nid', $project->id())
      ->fields('pr');
    $info = $query->execute()->fetchAssoc();

    return [
      'drupal_project_nid' => $info['drupal_project_nid'] ?? NULL,
      'gitlab_project_id' => $info['gitlab_project_id'] ?? NULL,
      'gitlab_namespace' => $info['gitlab_namespace'] ?? NULL,
      'gitlab_project_name' => $info['gitlab_project_name'] ?? NULL,
    ];
  }

  /**
   * Get a Drupal project from the GitLab path.
   *
   * @param string $path
   *   Path within GitLab.
   *
   *   It can be a GitLab project ID, the project name, or the
   *   namespace/project_name.
   *
   * @return \Drupal\node\NodeInterface|null
   *   Project node or null.
   */
  public function getProjectByRepositoryPath($path): ?NodeInterface {
    $gitlab_project_id = is_numeric($path) ? (int) $path : NULL;
    // Assume 'project' namespace if none given.
    $namespace = 'project';
    $name = !is_numeric($path) ? $path : NULL;
    if (str_contains($path, '/')) {
      [$namespace, $name] = explode('/', $path);
    }
    if (!$gitlab_project_id && !$name) {
      return NULL;
    }

    $query = $this->connection
      ->select('drupalorg_project_repositories', 'pr')
      ->fields('pr', ['drupal_project_nid']);
    if ($gitlab_project_id) {
      $query->condition('pr.gitlab_project_id', $gitlab_project_id);
    }
    else {
      $query->condition('pr.gitlab_namespace', $namespace);
      $query->condition('pr.gitlab_project_name', $name);
    }
    $project = $query->execute()->fetchCol();
    if (count($project) === 1) {
      return $this->entityTypeManager->getStorage('node')->load($project[0]);
    }

    return NULL;
  }

  /**
   * Return the types of node that are considered projects.
   *
   * @return array
   *   Types of project.
   */
  public function projectNodeTypes(): array {
    return self::PROJECT_TYPES;
  }

  /**
   * Return whether a node is a project type or not.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Node to check.
   *
   * @return bool
   *   True if it's a project, false otherwise.
   */
  public function isProject(NodeInterface $node): bool {
    return in_array($node->bundle(), $this->projectNodeTypes());
  }

  /**
   * Determine if a given node is a sandbox or full project.
   *
   * @param \Drupal\node\NodeInterface $node
   *   A fully-loaded node object representing the project.
   *
   * @return bool
   *   TRUE if the given node is a sandbox project, FALSE if full.
   */
  public function isSandbox(NodeInterface $node): bool {
    if ($node->hasField('field_project_type')) {
      $project_type = $node->get('field_project_type')->value ?? NULL;
      return $project_type === 'sandbox';
    }
    return FALSE;
  }

  /**
   * Get “branches” for a project.
   *
   * Each branch identifier is everything up to and including the final dot in
   * the version number.
   *
   * @param \Drupal\node\NodeInterface $project_node
   *   Project node.
   * @param bool $only_supported
   *   Only return the supported branches if TRUE, otherwise all.
   *
   * @return \stdClass[]
   *   Array keyed by branch.
   *
   * @throws \Exception
   */
  public function getVersions(NodeInterface $project_node, $only_supported = FALSE): array {
    $query = $this->connection->select('drupalorg_project_release_supported_versions', 'prsv')
      ->fields('prsv', ['branch', 'supported', 'recommended_release', 'latest_release', 'latest_security_release'])
      ->condition('prsv.nid', $project_node->id());
    if ($only_supported) {
      $query->condition('prsv.supported', TRUE);
    }
    return $query->execute()->fetchAllAssoc('branch');
  }

  /**
   * Gets project usage information broken down by branch and date.
   *
   * @param \Drupal\node\NodeInterface $project_node
   *   Project node.
   * @param int $week
   *   Timestamp of the desired week.
   * @param bool $only_current
   *   Only include modern Drupal usage data.
   *
   * @return \stdClass[]
   *   Array keyed by branch.
   */
  public function getWeeklyUsage(NodeInterface $project_node, int $week, bool $only_current = TRUE): array {
    $query = $this->connection
      ->select('project_usage_week_release', 'puwr')
      ->fields('puwr', ['project_id', 'release_id', 'count', 'timestamp'])
      ->condition('puwr.project_id', $project_node->id())
      ->condition('puwr.timestamp', $week);

    $results = $query
      ->execute()
      ->fetchAllAssoc('release_id');
    if (empty($results)) {
      return [];
    }

    $raw_usage = [];
    foreach ($results as $row) {
      $raw_usage[$row->release_id] = $row->count;
    }
    $release_ids = array_keys($raw_usage);
    $releases = Node::loadMultiple($release_ids);

    $branches_usage = [];
    if (!empty($releases) && !empty($raw_usage)) {
      foreach ($releases as $release) {
        if ($this->includeInResults($release, $only_current)) {
          $branch = $this->getBranchFromVersion($release);
          if ($branch) {
            if (empty($branches_usage[$branch])) {
              $branches_usage[$branch] = 0;
            }
            $branches_usage[$branch] += $raw_usage[$release->id()];
          }
        }
      }
    }

    return $branches_usage;
  }

  /**
   * Gets project usage information broken down by branch and date.
   *
   * @param \Drupal\node\NodeInterface $project_node
   *   Project node.
   * @param bool $only_current
   *   Include only modern Drupal usage.
   * @param int|null $weeks
   *   If given, only data newer than the give "weeks" ago.
   *
   * @return \stdClass[]
   *   Array keyed by branch.
   */
  public function getTotalUsage(NodeInterface $project_node, bool $only_current = TRUE, ?int $weeks = NULL): array {
    $query = $this->connection
      ->select('project_usage_week_release', 'puwr')
      ->fields('puwr', ['project_id', 'release_id', 'count', 'timestamp'])
      ->condition('puwr.project_id', $project_node->id());
    if ($weeks) {
      $query->condition('puwr.timestamp', $weeks, '>');
    }

    // All time data.
    $results = $query
      ->execute()
      ->fetchAll();
    if (empty($results)) {
      return [];
    }

    // The below logic is very similar to the one in "getWeeklyUsage" but the
    // array structures are different, sorted by timestamps.
    $raw_usage = [];
    $branches_usage = [];
    $releases = [];
    foreach ($results as $row) {
      // Store raw usage.
      if (empty($raw_usage[$row->timestamp])) {
        $raw_usage[$row->timestamp] = [];
      }
      $raw_usage[$row->timestamp][$row->release_id] = $row->count;

      // Fill up releases array.
      if (!isset($releases[$row->release_id])) {
        $releases[$row->release_id] = Node::load($row->release_id);
      }

      // And prepare the branch arrays to hold the results.
      if (empty($branches_usage[$row->timestamp])) {
        $branches_usage[$row->timestamp] = [];
      }
      $release = $releases[$row->release_id] ?? NULL;
      if ($release && $this->includeInResults($release, $only_current)) {
        $branch = $this->getBranchFromVersion($release);
        if ($branch) {
          if (empty($branches_usage[$row->timestamp][$branch])) {
            $branches_usage[$row->timestamp][$branch] = 0;
          }
          $branches_usage[$row->timestamp][$branch] += $raw_usage[$row->timestamp][$release->id()];
        }
      }
    }

    return $branches_usage;
  }

  /**
   * Returns a branch name from the version field of a release.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Release to check.
   *
   * @return string
   *   Name of the branch.
   */
  protected function getBranchFromVersion(NodeInterface $node): string {
    $branch = NULL;
    $version = $node->get('field_release_version')->getValue();
    if ($version) {
      $version = $version[0]['value'] ?? NULL;
      if (!is_null($version)) {
        $branch = $this->getBranchFromReleaseVersion($version) . 'x';
      }
    }

    return $branch;
  }

  /**
   * Calculate if a release should be included in the results.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Release to check.
   * @param bool $only_current
   *   Include only modern Drupal or not.
   *
   * @return bool
   *   Result of the evaluation.
   */
  protected function includeInResults(NodeInterface $node, bool $only_current): bool {
    $include_in_result = FALSE;
    if ($node->isPublished() && $node->hasField('field_release_category')) {
      $category = $node->get('field_release_category')->getValue()[0]['value'] ?? NULL;
      if ($only_current && $category === 'current') {
        // Only modern Drupal releases.
        $include_in_result = TRUE;
      }
      elseif (!$only_current) {
        // All releases.
        $include_in_result = TRUE;
      }
    }

    return $include_in_result;
  }

  /**
   * Get the “branch” of a version. For grouping releases, not VCS.
   *
   * @param string $version
   *   The version number to process.
   *
   * @return string
   *   The version number with everything after the last '.' stripped.
   */
  protected function getBranchFromReleaseVersion($version) {
    return preg_replace('#\.[^.]*$#', '.', $version);
  }

  /**
   * Update the project logo URL, if needed.
   *
   * @param \Drupal\node\NodeInterface $project_node
   *   Project node.
   * @param string $avatar_url
   *   'avatar_url' value from GitLab project API.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function updateLogo(NodeInterface $project_node, string $avatar_url): void {
    $existing_logo = $project_node->get('field_logo_url')->uri;
    if ($existing_logo != $avatar_url) {
      $this->logger->notice('Updating @project logo from "@existing_logo" → "@to"', [
        '@project' => $project_node->get('field_project_machine_name')->value,
        '@existing_logo' => $existing_logo ?? '',
        '@to' => $avatar_url,
      ]);
      $project_node->set('field_logo_url', $avatar_url)->save();
    }
  }

  /**
   * Get project type information.
   *
   * @param \Drupal\node\NodeInterface $project_node
   *   Project node.
   *
   * @return array
   *   Associative array with 'singular', 'plural', and 'url' keys.
   */
  public function getProjectType(NodeInterface $project_node): array {
    $default_url = Url::fromUserInput('/project/' . $project_node->bundle());
    switch ($project_node->bundle()) {
      case 'project_general':
        if (
          is_array($project_node->get('field_project_composer_types')->value) &&
          in_array('drupal-recipe', $project_node->get('field_project_composer_types')->value)
        ) {
          return [
            'singular' => t('Recipe'),
            'plural' => t('Recipes'),
            'url' => Url::fromUserInput('/browse/recipes'),
          ];
        }
        else {
          return [
            'singular' => t('General project'),
            'plural' => t('General projects'),
            'url' => $default_url,
          ];
        }

      case 'project_module':
        return [
          'singular' => 'Module',
          'plural' => 'Modules',
          'url' => $default_url,
        ];

      case 'project_theme':
        return [
          'singular' => 'Theme',
          'plural' => 'Themes',
          'url' => $default_url,
        ];

      case 'project_distribution':
        return [
          'singular' => 'Distribution',
          'plural' => 'Distributions',
          'url' => $default_url,
        ];

      case 'project_theme_engine':
        return [
          'singular' => 'Theme engine',
          'plural' => 'Theme engines',
          'url' => $default_url,
        ];

      case 'project_drupalorg':
        return [
          'singular' => 'Community project',
          'plural' => 'Community projects',
          'url' => $default_url,
        ];

      case 'project_translation':
        return [
          'singular' => 'Translation',
          'plural' => 'Translations',
          'url' => $default_url,
        ];

      case 'project_core':
        return [
          'singular' => 'Drupal Core',
          'plural' => 'Drupal Cores',
          'url' => $default_url,
        ];
    }

    throw new \Exception('The entity is not a project.');
  }

}

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

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