reviewer-1.2.x-dev/src/Drush/Commands/ReviewerCommands.php

src/Drush/Commands/ReviewerCommands.php
<?php

namespace Drupal\reviewer\Drush\Commands;

use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\reviewer\Plugin\reviewer\Review\ReviewPluginInterface;
use Drupal\reviewer\Plugin\ReviewManagerInterface;
use Drupal\reviewer\Reviewer\Action;
use Drupal\reviewer\Reviewer\IgnorerInterface;
use Drupal\reviewer\Reviewer\Result\ResultFactoryInterface;
use Drupal\reviewer\Reviewer\Result\ResultInterface;
use Drupal\reviewer\Reviewer\Review\ReviewRunnerInterface;
use Drupal\reviewer\Reviewer\Status\Status;
use Drupal\reviewer\Reviewer\Status\StatusEvaluatorInterface;
use Drupal\reviewer\Reviewer\Status\StatusFactoryInterface;
use Drush\Attributes as CLI;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
use Drush\Symfony\BufferedConsoleOutput;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleOutputInterface;

/**
 * Drush commands for reviewer.
 */
final class ReviewerCommands extends DrushCommands {

  use AutowireTrait;

  // phpcs:ignore Drupal.Commenting.FunctionComment.Missing
  public function __construct(
    private readonly IgnorerInterface $ignorer,
    private readonly ResultFactoryInterface $resultFactory,
    private readonly ReviewManagerInterface $reviewManager,
    private readonly ReviewRunnerInterface $reviewRunner,
    private readonly StatusEvaluatorInterface $statusEvaluator,
    private readonly StatusFactoryInterface $statusFactory,
  ) {
    parent::__construct();
  }

  /**
   * List all available reviews.
   */
  #[CLI\Command(name: 'reviewer:list', aliases: ['rvl'])]
  #[CLI\Help(description: 'List available reviews.')]
  #[CLI\Usage(name: 'reviewer:list', description: 'List available reviews.')]
  #[CLI\DefaultTableFields(fields: ['id', 'label'])]
  #[CLI\FieldLabels(labels: ['id' => 'ID', 'label' => 'Label'])]
  public function list(): RowsOfFields|null {
    $reviews = $this->reviewManager->createAllInstances();
    if ($reviews) {
      $this->io()->writeln('');
      $this->io()->writeln(dt('Available Reviews:'));
      return new RowsOfFields(array_map(
        fn(ReviewPluginInterface $review) => ['id' => $review->getPluginId(), 'label' => $review->getLabel()],
        $reviews,
      ));
    }

    $this->io()->warning(dt('No reviews available.'));
    return NULL;
  }

  /**
   * Run reviews, checking for issues.
   *
   * @param string[] $ids
   * @param array<string, bool> $options
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  #[CLI\Command(name: 'reviewer:check', aliases: ['rvc'])]
  #[CLI\Help(description: 'Run reviews, checking for issues.')]
  #[CLI\Argument(name: 'ids', description: 'Review IDs to run, separated by spaces.')]
  #[ClI\Option(name: 'show-passed', description: 'Display passed tests in addition to failures and errors.')]
  #[ClI\Option(name: 'show-not-run', description: 'Display tests which were not run in addition to failures and errors.')]
  #[ClI\Option(name: 'show-ignored', description: 'Display ignored tests in addition to failures and errors.')]
  #[ClI\Option(name: 'show-all', description: 'Display ignored and passed tests in addition to failures and errors.')]
  #[CLI\Usage(name: 'reviewer:run', description: 'Run all reviews.')]
  #[CLI\Usage(name: 'reviewer:run node', description: 'Run the "node" review.')]
  #[CLI\Usage(name: 'reviewer:run node:article,page paragraph:gallery', description: 'Run reviews for the specified "node" and "paragraph" bundles.')]
  public function check(
    array $ids,
    array $options = [
      'show-ignored' => FALSE,
      'show-passed' => FALSE,
      'show-not-run' => FALSE,
      'show-all' => FALSE,
    ],
  ): void {
    $show_ignored = $options['show-ignored'] || $options['show-all'];
    $show_passed = $options['show-passed'] || $options['show-all'];
    $show_not_run = $options['show-not-run'] || $options['show-all'];

    foreach ($this->doRun($ids, Action::Check) as $review) {
      $rows = [];

      foreach ($review->getResults()->getIndividualResults() as $result) {
        if ($show_passed && $this->statusEvaluator->isPass($result->getStatus())) {
          $rows[] = $result;
          continue;
        }

        if ($show_not_run && $this->statusEvaluator->isNotRun($result->getStatus())) {
          $rows[] = $result;
          continue;
        }

        if ($show_ignored && $this->ignorer->isIgnored($result)) {
          $rows[] = $this->resultFactory->createResult(
            $result->getId(),
            $result->getStatus(),
            $this->ignorer->ignoredReason($result),
          );
          continue;
        }

        if (
          $this->statusEvaluator->isFailureOrError($result->getStatus())
          && !$this->ignorer->isIgnored($result)
        ) {
          $rows[] = $result;
        }
      }

      $this->tableFromResults($rows, $review->getLabel());
    }
  }

  /**
   * Fix failures found in reviews.
   *
   * @param string[] $ids
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  #[CLI\Command(name: 'reviewer:fix', aliases: ['rvf'])]
  #[CLI\Help(description: 'Fix failures found by reviews. Failures marked as ignored will not be fixed.')]
  #[CLI\Argument(name: 'ids', description: 'Review IDs to fix failures in, separated by spaces.')]
  #[CLI\Usage(name: 'reviewer:fix', description: 'Fix failures in all reviews.')]
  #[CLI\Usage(name: 'reviewer:run node', description: 'Fix failures found in the "node" review.')]
  #[CLI\Usage(name: 'reviewer:run node:article,page paragraph:gallery', description: 'Fix failures found in the specified bundles of the "node" and "paragraph" reviews.')]
  public function fix(array $ids): void {
    $confirm = $this->io()->confirm(dt('Fixing issues will alter configuration on your site, and cannot be undone.'));
    if (!$confirm) {
      return;
    }

    foreach ($this->doRun($ids, Action::Fix) as $review) {
      $results = [];
      foreach ($review->getResults()->getIndividualResults() as $result) {
        if ($result->getStatus() === Status::Fixed) {
          $results[] = $result;
        }
      }
      $this->tableFromResults($results, $review->getLabel());
    }
  }

  /**
   * Ignore failures and errors found in reviews.
   *
   * @param string[] $ids
   * @param array<string, bool> $options
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  #[CLI\Command(name: 'reviewer:ignore', aliases: ['rvi'])]
  #[CLI\Help(description: 'Run reviews, prompting to ignore any unignored failures and errors.')]
  #[CLI\Argument(name: 'ids', description: 'Review IDs to run, separated by spaces.')]
  #[CLI\Option(name: 'existing', description: 'Review and update existing ignored failures and errors as well as unignored ones.')]
  #[CLI\Option(name: 'reset', description: 'Unignore previously ignored failures and errors before ignoring.')]
  #[CLI\Option(name: 'unignore', description: 'Unignore previously ignored failures and errors.')]
  public function ignore(
    array $ids,
    array $options = [
      'existing' => FALSE,
      'reset' => FALSE,
      'unignore' => FALSE,
    ],
  ): void {
    $review_existing = (bool) $options['existing'];
    $do_reset = $options['reset'] || $options['unignore'];
    $unignore = (bool) $options['unignore'];

    $summary = new BufferedConsoleOutput(decorated: TRUE);

    if ($do_reset || $unignore) {
      foreach ($this->doRun($ids, Action::Check) as $review) {
        $this->ignorer->unignore($review->getResults()->getIndividualResults());
      }
    }
    if ($unignore) {
      return;
    }

    if (!$this->input()->isInteractive()) {
      throw new \RuntimeException('reviewer:ignore can only be run with the --unignore option without an interactive terminal.');
    }

    foreach ($this->doRun($ids, Action::Check) as $review) {
      $results = array_filter(
        $review->getResults()->getIndividualResults(),
        fn(ResultInterface $result) => $this->statusEvaluator->isFailureOrError($result->getStatus()),
      );
      if ($review_existing) {
        $results += array_filter(
          $review->getResults()->getIndividualResults(),
          fn(ResultInterface $result) => $this->statusEvaluator->isIgnored($result->getStatus()) && $this->ignorer->isConfigIgnored($result),
        );
      }
      ksort($results);

      foreach ($results as $result) {
        if ($this->statusEvaluator->isPass($result->getStatus())) {
          continue;
        }

        $is_ignored = FALSE;
        $is_updated = FALSE;
        $reason = '';

        if (
          $review_existing
          && $this->ignorer->isConfigIgnored($result)
        ) {
          $this->tableFromResults([$result], $review->getLabel());
          $reason = $this->ignorer->ignoredReason($result);
          $is_ignored = $this->io()->confirm(dt('Keep ignoring result @id?' . PHP_EOL . ' Current ignored reason: @reason', [
            '@id' => $result->getId(),
            '@reason' => $reason,
          ]));
          $is_updated = TRUE;
        }

        if (!$this->ignorer->isIgnored($result)) {
          $this->tableFromResults([$result], $review->getLabel());
          $is_ignored = $this->io()->confirm(
            dt('Ignore result @id?', ['@id' => $result->getId()]),
            FALSE,
          );
          $is_updated = TRUE;
        }

        if (!$is_updated) {
          continue;
        }

        if ($is_ignored) {
          $reason = $this->io()->ask(
            dt('Provide a reason for ignoring @id.', [
              '@id' => $result->getId(),
            ]),
            $reason,
          );
          assert(\is_string($reason));

          $this->ignorer->ignore($result, $reason);
          $results[$result->getId()] = $this->resultFactory->createResult(
            $result->getId(),
            $this->statusFactory->createIgnored($result->getStatus()),
            $reason,
          );
        }
        else {
          $this->ignorer->unignore($result);
          $results[$result->getId()] = $this->resultFactory->createResult(
            $result->getId(),
            $this->statusFactory->createUnignored($result->getStatus()),
            $result->getMessage(),
          );
        }
      }

      $this->tableFromResults($results, $review->getLabel(), $summary);
    }

    echo $summary->fetch();
  }

  /**
   * Run review plugins.
   *
   * @param string[] $ids
   * @param \Drupal\reviewer\Reviewer\Action $action
   *
   * @return array<string, \Drupal\reviewer\Reviewer\Review\ReviewInterface>
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  private function doRun(array $ids, Action $action): array {
    $reviews = [];
    if ($review_ids = $this->processReviewIds($ids)) {
      foreach ($review_ids as $review_id => $bundles) {
        if (!$this->reviewManager->hasDefinition($review_id)) {
          $this->io()->warning(dt('Review @id does not exist.', [
            '@id' => $review_id,
          ]));
        }

        $reviews = array_merge($reviews, $this->reviewRunner->runIds(
          $action,
          [$review_id],
          $bundles,
        ));
      }
    }
    else {
      $reviews = $this->reviewRunner->runIds($action);
    }
    return $reviews;
  }

  /**
   * Process review IDs and return an array of review IDs and their bundles.
   *
   * @param string[] $review_ids
   *
   * @return array<string, string[]>
   *   Array keys are the review IDs with the values the bundles for each review
   *   ID, with an empty array for all bundles or a review which does not
   *   support bundles.
   */
  private function processReviewIds(array $review_ids): array {
    $processed_ids = [];
    foreach ($review_ids as $review_id) {
      $split_ids = explode(':', $review_id);
      $bundles = match (\count($split_ids)) {
        1 => [],
        default => explode(',', $split_ids[1]),
      };
      $processed_ids = array_merge(
        $processed_ids,
        array_fill_keys(explode(',', $split_ids[0]), $bundles),
      );
    }
    return $processed_ids;
  }

  /**
   * Print a table from an array of results.
   *
   * @param \Drupal\reviewer\Reviewer\Result\IndividualResultInterface[] $rows
   */
  private function tableFromResults(
    array $rows,
    string $label,
    ConsoleOutputInterface $output = new ConsoleOutput(),
  ): void {
    if ($rows) {
      $table = (new Table($output))
        ->setStyle($this->io()->createTable()->getStyle())
        ->setVertical()
        ->setColumnWidth(0, mb_strlen($label) + 4)
        ->setHeaderTitle($label)
        ->setHeaders([dt('Status'), dt('ID'), dt('Message')]);

      foreach ($rows as $row) {
        $table->addRow([
          $this->coloredLabel($row->getStatus()),
          $row->getId(),
          $row->getMessage(),
        ]);
      }

      $output->writeln('');
      $table->render();
      $output->writeln('');
    }
  }

  /**
   * Get a colored label for a status.
   */
  private function coloredLabel(Status $status): string {
    $label = (string) $status->label();
    return match ($status) {
      Status::NotRun => "<fg=black;bg=white>$label</>",
      Status::IgnoredFailure, Status::IgnoredError => "<fg=black;bg=yellow>$label</>",
      Status::Pass, Status::Fixed => "<info>$label</info>",
      Status::Fail, Status::Error => "<error>$label</error>",
    };
  }

}

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

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