lightning_scheduler-8.x-1.x-dev/src/TransitionManager.php
src/TransitionManager.php
<?php
namespace Drupal\lightning_scheduler;
use Drupal\Component\Serialization\Json;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
/**
* @internal
* This is an internal part of Lightning Scheduler and may be changed or
* removed at any time without warning. It should not be used by external
* code in any way.
*/
final class TransitionManager {
use StringTranslationTrait;
/**
* The moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
private $moderationInformation;
/**
* The currently logged-in user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
private $currentUser;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
private $logger;
/**
* State storage service.
*
* @var \Drupal\Core\State\StateInterface
*/
private $state;
/**
* TransitionManager constructor.
*
* @param ModerationInformationInterface $moderation_information
* The moderation information service.
* @param AccountInterface $current_user
* The currently logged-in user.
* @param EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param LoggerChannelInterface $logger
* The logger channel.
* @param TranslationInterface $translation
* The string translation service.
* @param StateInterface $state
* State storage service.
*/
public function __construct(
ModerationInformationInterface $moderation_information,
AccountInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
LoggerChannelInterface $logger,
TranslationInterface $translation,
StateInterface $state
) {
$this->moderationInformation = $moderation_information;
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->logger = $logger;
$this->setStringTranslation($translation);
$this->state = $state;
}
/**
* Validates incoming transition data.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @see lightning_scheduler_form_alter()
*/
public static function validate(array $element, FormStateInterface $form_state) {
$data = Json::decode($element['#value']);
if (json_last_error() !== JSON_ERROR_NONE) {
$variables = [
'%error' => json_last_error_msg(),
];
$form_state->setError($element, t('Invalid transition data: %error', $variables));
return;
}
if (! is_array($data)) {
$form_state->setError($element, t('Expected scheduled transitions to be an array.'));
return;
}
$minimum_date = NULL;
if (!\Drupal::config('lightning_scheduler.settings')->get('allow_past_dates')) {
$minimum_date = \Drupal::time()->getRequestTime();
}
foreach ($data as $transition) {
if (empty($transition['when'])) {
$form_state->setError($element, t('Scheduled transitions must have a date and time.'));
return;
}
if (! preg_match('/^[0-9]+$/', $transition['when'])) {
$variables = [
'%when' => $transition['when'],
];
$form_state->setError($element, t('"%when" is not a valid date and time.', $variables));
return;
}
// The transition must take place after $minimum_date.
if (isset($minimum_date) && $transition['when'] < $minimum_date) {
$form_state->setError($element, t('You cannot schedule a transition to take place before @date.', [
'@date' => self::renderTranslatableDate($minimum_date, 'long'),
]));
return;
}
$minimum_date = $transition['when'];
}
}
/**
* Executes all scheduled transitions for a particular entity type.
*
* @param string $entity_type_id
* The entity type ID.
* @param DrupalDateTime $now
* The time that processing began.
*/
public function process($entity_type_id, DrupalDateTime $now) {
/** @var ContentEntityInterface $entity */
foreach ($this->getTransitionable($entity_type_id, $now) as $entity) {
$error_context = [
'entity_type' => (string) $entity->getEntityType()->getSingularLabel(),
'entity' => $entity->label(),
];
$workflow = $this->moderationInformation->getWorkflowForEntity($entity);
// If the entity hasn't got a workflow, what are we doing here?
if (empty($workflow)) {
$message = $this->t('Could not execute scheduled transition(s) for {entity_type} "{entity}" because no workflow is assigned to it.');
$this->logger->error($message, $error_context);
continue;
}
$transition_set = new TransitionSet(
$entity->get('scheduled_transition_date'),
$entity->get('scheduled_transition_state')
);
$to_state = $transition_set->getExpectedState($now);
// If no workflow state is targeted, there's nothing to transition to.
if (empty($to_state)) {
continue;
}
$from_state = $entity->moderation_state->value;
$plugin = $workflow->getTypePlugin();
if ($plugin->hasTransitionFromStateToState($from_state, $to_state)) {
$entity->set('moderation_state', $to_state);
}
else {
$error_context += [
'from_state' => $plugin->getState($from_state)->label(),
'to_state' => $plugin->getState($to_state)->label(),
'workflow' => $workflow->label(),
];
$message = $this->t('Could not transition {entity_type} "{entity}" from {from_state} to {to_state} because no such transition exists in the "{workflow}" workflow.');
$this->logger->warning($message, $error_context);
}
$transition_set->trim($now);
$entity->save();
}
}
/**
* Returns all transitionable entities of a given type.
*
* The entity type is assumed to have the scheduled_transition_date field.
*
* @param string $entity_type_id
* The entity type ID.
* @param DrupalDateTime $now
* The time that processing began.
*
* @return \Generator
* An iterable of the latest revisions of all transitionable entities of the
* given type.
*/
private function getTransitionable($entity_type_id, DrupalDateTime $now) {
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$sql_timezone = DateTimeItemInterface::STORAGE_TIMEZONE;
$storage_format = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
$now = $now->format($storage_format, ['timezone' => $sql_timezone]);
$time_ago = 0;
$cron_last = $this->state->get('system.cron_last');
// Three days before cron last.
if (!empty($cron_last)) {
$time_ago = $cron_last - 259200;
}
$time_ago_in_drupal_datetime = new DrupalDateTime(date('Y-m-d\TH:i:s', $time_ago), $sql_timezone);
$time_ago_in_drupal_datetime_for_query = $time_ago_in_drupal_datetime->format($storage_format, ['timezone' => $sql_timezone]);
// Entities are transitionable if their latest revision has any transitions
// scheduled now or going back to a little bit before the last cron run.
// We should not reprocess going back to 1970 beginning of unix time.
// Limit past re-processing for performance and scalability reasons.
// Only go back in time as far as the last successful cron.
$ids = $storage->getQuery()
->latestRevision()
->accessCheck(FALSE)
->condition('scheduled_transition_date.value', $now, '<=')
->condition('scheduled_transition_date.value', $time_ago_in_drupal_datetime_for_query, '>=')
->execute();
foreach (array_keys($ids) as $revision_id) {
yield $storage->loadRevision($revision_id);
}
}
/**
* Render a date from epoch using a translatable date format in Drupal.
*
* @param int $timestamp
* The epoch timestamp.
* @param string $format_id
* The date format ID to use.
*
* @return string
* The formatted date string.
*/
private static function renderTranslatableDate($timestamp, $format_id) {
// Load the date format storage service
$date_format_storage = \Drupal::entityTypeManager()->getStorage('date_format');
// Check if the date format ID exists
$dateFormat = $date_format_storage->load($format_id);
// Convert epoch to DateTime object
$dateTime = new \DateTime();
$dateTime->setTimestamp($timestamp);
// Load the date formatter service
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = \Drupal::service('date.formatter');
// Render the date using the format ID
return (!empty($dateFormat)) ?
$date_formatter->format($dateTime->getTimestamp(), $format_id) :
$date_formatter->format($dateTime->getTimestamp(), 'custom', 'F j, Y g:i A');
}
}
