reviewer-1.2.x-dev/src/Reviewer/Task/TaskBase.php
src/Reviewer/Task/TaskBase.php
<?php
declare(strict_types=1);
namespace Drupal\reviewer\Reviewer\Task;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\reviewer\Attribute\Task as TaskAttribute;
use Drupal\reviewer\Exception\AttributeMissingException;
use Drupal\reviewer\Reviewer\Action;
use Drupal\reviewer\Reviewer\Checklist\ChecklistInterface;
use Drupal\reviewer\Reviewer\Result\CollectionResultInterface;
use Drupal\reviewer\Reviewer\Result\IndividualResultInterface;
use Drupal\reviewer\Reviewer\Result\ResultFactoryInterface;
use Drupal\reviewer\Reviewer\Status\Status;
use Drupal\reviewer\Reviewer\Status\StatusEvaluatorInterface;
use Drupal\reviewer\Reviewer\Status\StatusFactoryInterface;
/**
* Provides a base class for tasks that all tasks should extend.
*
* Tasks are the basic unit of work in reviewer, responsible for checking for
* correct and incorrect configuration values.
*
* Create tasks in the \Drupal\my_module\Plugin\reviewer\Task namespace. Task
* classes must have the \Drupal\reviewer\Attribute\Task attribute.
*
* Tasks implementing \Drupal\reviewer\Reviewer\Task\FixableInterface can have
* failures automatically fixed by reviewer.
*
* This class provides methods required by reviewer to run reviews, as well as
* some convenience methods for creating results from tasks and loading
* configuration defined in the reviews. To jump start creating tasks, check out
* the base tasks provided by the reviewer test kit module, which allow you to
* create checks and fixes through class properties alone:
*
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\ConfigTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\Form\FieldWidgetSettingsTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\Form\FieldWidgetsTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\Form\FormFieldGroupSettingsTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\Form\FormFieldsDisabledTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\View\FieldFormatterSettingsTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\View\FieldFormattersTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\View\ViewFieldGroupSettingsTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\View\ViewFieldsDisabledTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\EntityTypeTaskBase
* @see \Drupal\reviewer_test_kit\Plugin\reviewer\Task\Entity\Display\DisplayTaskBase
*
* Here is an example of a simple task which checks and fixes some
* configuration:
*
* @code
* #[Task('example')]
* final class Example extends TaskBase implements FixableInterface {
*
* public function check(): ResultInterface {
* $config = $this->configFactory->get('reviewer.example');
*
* return $this->createCheckResult(
* $config->get('item') === 'correct',
* 'Config reviewer.example.item has the correct value.',
* 'Config reviewer.example.item has an incorrect value.',
* );
* }
*
* public function fix(): ResultInterface {
* $config = $this->configFactory->getEditable('reviewer.example');
*
* if ($config->get('item') !== 'correct') {
* $config->set('item', 'example')->save();
* }
*
* return $this->createFixResult(
* // Run the check again to verify changes.
* $this->check()->getStatus(),
* 'Fixed reviewer.example configuration.',
* 'Unable to fix reviewer.example configuration.',
* );
* }
*
* }
* @endcode
*
* It is possible for tasks to declare module dependencies so that errors are
* not reported unnecessarily when running reviews. Tasks that do not meet their
* module dependency return the \Drupal\reviewer\Reviewer\Status\Status::NotRun
* status. This allows reviews to still run and not create errors on
* environments which may not have the module installed, or for the re-use of
* reviews across projects without having to define new checklists and reviews.
*
* Here is an example attribute of a task which depends on the workflows module:
*
* @code
* #[Task(
* id: 'example',
* module_dependencies: ['workflows'],
* )]
* final class Example extends TaskBase implements FixableInterface {}
* @endcode
*
* @see \Drupal\reviewer\Attribute\Task
* @see \Drupal\reviewer\Reviewer\Result\ResultInterface
* @see \Drupal\reviewer\Reviewer\Result\CollectionResultInterface
*/
abstract class TaskBase implements TaskInterface {
private TaskAttribute $taskAttribute;
private ChecklistInterface $checklist;
protected CollectionResultInterface $results;
// phpcs:ignore Drupal.Commenting.FunctionComment.Missing
public function __construct(
protected readonly EntityDisplayRepositoryInterface $entityDisplayRepository,
protected readonly EntityFieldManagerInterface $entityFieldManager,
protected readonly EntityTypeManagerInterface $entityTypeManager,
protected readonly ConfigFactoryInterface $configFactory,
private readonly ModuleHandlerInterface $moduleHandler,
private readonly ResultFactoryInterface $resultFactory,
protected readonly StatusEvaluatorInterface $statusEvaluator,
protected readonly StatusFactoryInterface $statusFactory,
) {}
/**
* Get the config entity loaded by the review this task is in.
*/
protected function getConfigEntity(): ConfigEntityInterface|null {
return $this->checklist->getConfigEntity();
}
/**
* Create an individual result.
*
* It is recommended to use the more specific createCheckResult() or
* createFixResult() over this method in most cases.
*/
protected function createResult(
Status $status,
string $message,
string $variant = '',
): IndividualResultInterface {
return $this->resultFactory->createResult(
$this->resultId($variant),
$status,
$message,
$this instanceof FixableInterface,
);
}
/**
* Create an individual result for a task.
*/
protected function createCheckResult(
bool $is_pass,
string $pass_message,
string $failure_message,
string $variant = '',
): IndividualResultInterface {
$message = $is_pass ? $pass_message : $failure_message;
return $this->resultFactory->createResult(
$this->resultId($variant),
$this->statusFactory->createFromBool($is_pass),
$message,
$this instanceof FixableInterface,
);
}
/**
* Create an individual result for a fix.
*/
protected function createFixResult(
Status $status,
string $success_message,
string $failure_message,
): IndividualResultInterface {
$status = $this->statusEvaluator->isPass($status) ? Status::Fixed : $status;
$message = $this->statusEvaluator->isFixed($status) ? $success_message : $failure_message;
return $this->resultFactory->createResult(
$this->resultId(),
$status,
$message,
$this instanceof FixableInterface,
);
}
/**
* Create a result collection.
*
* @param \Drupal\reviewer\Reviewer\Result\ResultInterface[] $results
*
* @return \Drupal\reviewer\Reviewer\Result\CollectionResultInterface
*/
protected function createCollection(array $results = []): CollectionResultInterface {
return $this->resultFactory->createCollection($this->resultId(), $results);
}
/**
* {@inheritdoc}
*/
public function run(Action $action): TaskInterface {
$unmet_dependencies = $this->checkDependencies();
if ($unmet_dependencies) {
$not_run = $this->createResult(
Status::NotRun,
sprintf('The following modules required by this task are not enabled: %s.', implode(', ', $unmet_dependencies)),
);
$this->results = $this->createCollection([$not_run]);
return $this;
}
try {
$result = $this->check();
if (
$action === Action::Fix
&& $this instanceof FixableInterface
&& $this->statusEvaluator->isFailure($result->getStatus())
&& !$this->statusEvaluator->isIgnored($result->getStatus())
) {
$result = $this->fix();
}
}
catch (\Exception $e) {
$result = $this->createResult(
Status::Error,
$e->getMessage(),
);
}
if ($result instanceof CollectionResultInterface) {
$this->results = $result;
}
else {
$this->results = $this->createCollection([$result]);
}
return $this;
}
/**
* Check that all module dependencies are satisfied and return all errors.
*
* @return string[]
*/
private function checkDependencies(): array {
$unmet_dependencies = [];
foreach ($this->getModuleDependencies() as $module) {
if (!$this->moduleHandler->moduleExists($module)) {
$unmet_dependencies[] = $module;
}
}
return $unmet_dependencies;
}
/**
* Get the task attribute for this task.
*
* @throws \Drupal\reviewer\Exception\AttributeMissingException
* Thrown when the task attribute is missing.
*/
private function getTaskAttribute(): TaskAttribute {
if (!isset($this->taskAttribute)) {
$attributes = (new \ReflectionClass($this))->getAttributes(TaskAttribute::class);
if (!$attributes) {
throw new AttributeMissingException(TaskAttribute::class, $this::class);
}
$this->taskAttribute = reset($attributes)->newInstance();
}
return $this->taskAttribute;
}
/**
* {@inheritdoc}
*/
public function getAttributeId(): string {
return $this->getTaskAttribute()->getId();
}
/**
* {@inheritdoc}
*/
public function getModuleDependencies(): array {
return $this->getTaskAttribute()->getModuleDependencies();
}
/**
* {@inheritdoc}
*/
public function getId(): string {
return "{$this->checklist->getId()}.{$this->getAttributeId()}";
}
/**
* {@inheritdoc}
*/
public function resultId(string $variant = ''): string {
$id = trim("{$this->checklist->resultId()}.{$this->getId()}.$variant", '.');
$id_parts = explode('.', $id);
$id_unique_parts = array_unique($id_parts);
return implode('.', $id_unique_parts);
}
/**
* {@inheritdoc}
*/
public function setChecklist(ChecklistInterface $checklist): TaskInterface {
$this->checklist = $checklist;
return $this;
}
/**
* {@inheritdoc}
*/
public function getStatus(): Status {
return $this->getResults()->getStatus();
}
/**
* {@inheritdoc}
*/
public function getResults(): CollectionResultInterface {
return $this->results;
}
/**
* {@inheritdoc}
*/
public function getIgnored(): array {
return $this->checklist->getIgnored();
}
}
