class SchedulerManager in Scheduler 8
Same name and namespace in other branches
- 2.x src/SchedulerManager.php \Drupal\scheduler\SchedulerManager
Defines a scheduler manager.
Hierarchy
- class \Drupal\scheduler\SchedulerManager uses StringTranslationTrait
Expanded class hierarchy of SchedulerManager
3 files declare their use of SchedulerManager
- LightweightCronController.php in src/
Controller/ LightweightCronController.php - SchedulerCommands.php in src/
Commands/ SchedulerCommands.php - SchedulerCronForm.php in src/
Form/ SchedulerCronForm.php
1 string reference to 'SchedulerManager'
1 service uses SchedulerManager
File
- src/
SchedulerManager.php, line 23
Namespace
Drupal\schedulerView source
class SchedulerManager {
use StringTranslationTrait;
/**
* Date formatter service object.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Scheduler Logger service object.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Module handler service object.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Entity Type Manager service object.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Config Factory service object.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The event dispatcher.
*
* @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
*/
protected $eventDispatcher;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* Constructs a SchedulerManager object.
*/
public function __construct(DateFormatterInterface $dateFormatter, LoggerInterface $logger, ModuleHandlerInterface $moduleHandler, EntityTypeManagerInterface $entityTypeManager, ConfigFactoryInterface $configFactory, ContainerAwareEventDispatcher $eventDispatcher, TimeInterface $time) {
$this->dateFormatter = $dateFormatter;
$this->logger = $logger;
$this->moduleHandler = $moduleHandler;
$this->entityTypeManager = $entityTypeManager;
$this->configFactory = $configFactory;
$this->eventDispatcher = $eventDispatcher;
$this->time = $time;
}
/**
* Dispatch a Scheduler event.
*
* All Scheduler events should be dispatched through this common function.
*
* Drupal 8.8 and 8.9 use Symfony 3.4 and from Drupal 9.0 the Symfony version
* is 4.4. Starting with Symfony 4.3 the signature of the event dispatcher
* function has the parameters swapped round, the event object is first,
* followed by the event name string. At 9.0 the existing signature has to be
* used but from 9.1 the parameters must be switched.
*
* @param \Drupal\Component\EventDispatcher\Event $event
* The event object.
* @param string $event_name
* The text name for the event.
*
* @see https://www.drupal.org/project/scheduler/issues/3166688
*/
public function dispatch(Event $event, string $event_name) {
// \Symfony\Component\HttpKernel\Kernel::VERSION will give the symfony
// version. However, testing this does not give the required outcome, we
// need to test the Drupal core version.
// @todo Remove the check when Core 9.1 is the lowest supported version.
if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
// The new way, with $event first.
$this->eventDispatcher
->dispatch($event, $event_name);
}
else {
// Replicate the existing dispatch signature.
$this->eventDispatcher
->dispatch($event_name, $event);
}
}
/**
* Publish scheduled nodes.
*
* @return bool
* TRUE if any node has been published, FALSE otherwise.
*
* @throws \Drupal\scheduler\Exception\SchedulerMissingDateException
* @throws \Drupal\scheduler\Exception\SchedulerNodeTypeNotEnabledException
*/
public function publish() {
$result = FALSE;
$action = 'publish';
// Select all nodes of the types that are enabled for scheduled publishing
// and where publish_on is less than or equal to the current time.
$nids = [];
$scheduler_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types($action));
if (!empty($scheduler_enabled_types)) {
$query = $this->entityTypeManager
->getStorage('node')
->getQuery()
->exists('publish_on')
->condition('publish_on', $this->time
->getRequestTime(), '<=')
->condition('type', $scheduler_enabled_types, 'IN')
->latestRevision()
->sort('publish_on')
->sort('nid');
// Disable access checks for this query.
// @see https://www.drupal.org/node/2700209
$query
->accessCheck(FALSE);
$nids = $query
->execute();
}
// Allow other modules to add to the list of nodes to be published.
$nids = array_unique(array_merge($nids, $this
->nidList($action)));
// Allow other modules to alter the list of nodes to be published.
$this->moduleHandler
->alter('scheduler_nid_list', $nids, $action);
// In 8.x the entity translations are all associated with one node id
// unlike 7.x where each translation was a separate node. This means that
// the list of node ids returned above may have some translations that need
// processing now and others that do not.
/** @var \Drupal\node\NodeInterface[] $nodes */
$nodes = $this
->loadNodes($nids);
foreach ($nodes as $node_multilingual) {
// The API calls could return nodes of types which are not enabled for
// scheduled publishing, so do not process these. This check can be done
// once, here, as the setting will be the same for all translations.
if (!$node_multilingual->type->entity
->getThirdPartySetting('scheduler', 'publish_enable', $this
->setting('default_publish_enable'))) {
throw new SchedulerNodeTypeNotEnabledException(sprintf("Node %d '%s' will not be published because node type '%s' is not enabled for scheduled publishing", $node_multilingual
->id(), $node_multilingual
->getTitle(), node_get_type_label($node_multilingual)));
}
$languages = $node_multilingual
->getTranslationLanguages();
foreach ($languages as $language) {
// The object returned by getTranslation() behaves the same as a $node.
$node = $node_multilingual
->getTranslation($language
->getId());
// If the current translation does not have a publish on value, or it is
// later than the date we are processing then move on to the next.
$publish_on = $node->publish_on->value;
if (empty($publish_on) || $publish_on > $this->time
->getRequestTime()) {
continue;
}
// Check that other modules allow the action on this node.
if (!$this
->isAllowed($node, $action)) {
continue;
}
// $node->setChangedTime($publish_on) will fail badly if an API call has
// removed the date. Trap this as an exception here and give a
// meaningful message.
// @todo This will now never be thrown due to the empty(publish_on)
// check above to cater for translations. Remove this exception?
if (empty($node->publish_on->value)) {
$field_definitions = $this->entityTypeManager
->getFieldDefinitions('node', $node
->getType());
$field = (string) $field_definitions['publish_on']
->getLabel();
throw new SchedulerMissingDateException(sprintf("Node %d '%s' will not be published because field '%s' has no value", $node
->id(), $node
->getTitle(), $field));
}
// Trigger the PRE_PUBLISH event so that modules can react before the
// node is published.
$event = new SchedulerEvent($node);
$this
->dispatch($event, SchedulerEvents::PRE_PUBLISH);
$node = $event
->getNode();
// Update 'changed' timestamp.
$node
->setChangedTime($publish_on);
$old_creation_date = $node
->getCreatedTime();
$msg_extra = '';
// If required, set the created date to match published date.
if ($node->type->entity
->getThirdPartySetting('scheduler', 'publish_touch', $this
->setting('default_publish_touch')) || $node
->getCreatedTime() > $publish_on && $node->type->entity
->getThirdPartySetting('scheduler', 'publish_past_date_created', $this
->setting('default_publish_past_date_created'))) {
$node
->setCreatedTime($publish_on);
$msg_extra = $this
->t('The previous creation date was @old_creation_date, now updated to match the publishing date.', [
'@old_creation_date' => $this->dateFormatter
->format($old_creation_date, 'short'),
]);
}
$create_publishing_revision = $node->type->entity
->getThirdPartySetting('scheduler', 'publish_revision', $this
->setting('default_publish_revision'));
if ($create_publishing_revision) {
$node
->setNewRevision();
// Use a core date format to guarantee a time is included.
$revision_log_message = rtrim($this
->t('Published by Scheduler. The scheduled publishing date was @publish_on.', [
'@publish_on' => $this->dateFormatter
->format($publish_on, 'short'),
]) . ' ' . $msg_extra);
$node
->setRevisionLogMessage($revision_log_message)
->setRevisionCreationTime($this->time
->getRequestTime());
}
// Unset publish_on so the node will not get rescheduled by subsequent
// calls to $node->save().
$node->publish_on->value = NULL;
// Invoke all implementations of hook_scheduler_publish_action() to
// allow other modules to do the "publishing" process instead of
// Scheduler.
$hook = 'scheduler_publish_action';
$processed = FALSE;
$failed = FALSE;
foreach ($this->moduleHandler
->getImplementations($hook) as $module) {
$function = $module . '_' . $hook;
$return = $function($node);
$processed = $processed || $return === 1;
$failed = $failed || $return === -1;
}
// Log the fact that a scheduled publication is about to take place.
$view_link = $node
->toLink($this
->t('View node'));
$node_type = $this->entityTypeManager
->getStorage('node_type')
->load($node
->bundle());
$node_type_link = $node_type
->toLink($this
->t('@label settings', [
'@label' => $node_type
->label(),
]), 'edit-form');
$logger_variables = [
'@type' => $node_type
->label(),
'%title' => $node
->getTitle(),
'link' => $node_type_link
->toString() . ' ' . $view_link
->toString(),
'@hook' => 'hook_' . $hook,
];
if ($failed) {
// At least one hook function returned a failure or exception, so stop
// processing this node and move on to the next one.
$this->logger
->warning('Publishing failed for %title. Calls to @hook returned a failure code.', $logger_variables);
continue;
}
elseif ($processed) {
// The node had 'publishing' processed by a module implementing the
// hook, so no need to do anything more, apart from log this result.
$this->logger
->notice('@type: scheduled processing of %title completed by calls to @hook.', $logger_variables);
}
else {
// None of the above hook calls processed the node and there were no
// errors detected so set the node to published.
$this->logger
->notice('@type: scheduled publishing of %title.', $logger_variables);
$node
->setPublished();
}
// Invoke the event to tell Rules that Scheduler has published the node.
if ($this->moduleHandler
->moduleExists('scheduler_rules_integration')) {
_scheduler_rules_integration_dispatch_cron_event($node, 'publish');
}
// Trigger the PUBLISH event so that modules can react after the node is
// published.
$event = new SchedulerEvent($node);
$this
->dispatch($event, SchedulerEvents::PUBLISH);
// Use the standard actions system to publish and save the node.
$node = $event
->getNode();
$action_id = 'node_publish_action';
if ($this->moduleHandler
->moduleExists('workbench_moderation_actions')) {
// workbench_moderation_actions module uses a custom action instead.
$action_id = 'state_change__node__published';
}
$this->entityTypeManager
->getStorage('action')
->load($action_id)
->getPlugin()
->execute($node);
$result = TRUE;
}
}
return $result;
}
/**
* Unpublish scheduled nodes.
*
* @return bool
* TRUE if any node has been unpublished, FALSE otherwise.
*
* @throws \Drupal\scheduler\Exception\SchedulerMissingDateException
* @throws \Drupal\scheduler\Exception\SchedulerNodeTypeNotEnabledException
*/
public function unpublish() {
$result = FALSE;
$action = 'unpublish';
// Select all nodes of the types that are enabled for scheduled unpublishing
// and where unpublish_on is less than or equal to the current time.
$nids = [];
$scheduler_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types($action));
if (!empty($scheduler_enabled_types)) {
$query = $this->entityTypeManager
->getStorage('node')
->getQuery()
->exists('unpublish_on')
->condition('unpublish_on', $this->time
->getRequestTime(), '<=')
->condition('type', $scheduler_enabled_types, 'IN')
->latestRevision()
->sort('unpublish_on')
->sort('nid');
// Disable access checks for this query.
// @see https://www.drupal.org/node/2700209
$query
->accessCheck(FALSE);
$nids = $query
->execute();
}
// Allow other modules to add to the list of nodes to be unpublished.
$nids = array_unique(array_merge($nids, $this
->nidList($action)));
// Allow other modules to alter the list of nodes to be unpublished.
$this->moduleHandler
->alter('scheduler_nid_list', $nids, $action);
/** @var \Drupal\node\NodeInterface[] $nodes */
$nodes = $this
->loadNodes($nids);
foreach ($nodes as $node_multilingual) {
// The API calls could return nodes of types which are not enabled for
// scheduled unpublishing. Do not process these.
if (!$node_multilingual->type->entity
->getThirdPartySetting('scheduler', 'unpublish_enable', $this
->setting('default_unpublish_enable'))) {
throw new SchedulerNodeTypeNotEnabledException(sprintf("Node %d '%s' will not be unpublished because node type '%s' is not enabled for scheduled unpublishing", $node_multilingual
->id(), $node_multilingual
->getTitle(), node_get_type_label($node_multilingual)));
}
$languages = $node_multilingual
->getTranslationLanguages();
foreach ($languages as $language) {
// The object returned by getTranslation() behaves the same as a $node.
$node = $node_multilingual
->getTranslation($language
->getId());
// If the current translation does not have an unpublish on value, or it
// is later than the date we are processing then move on to the next.
$unpublish_on = $node->unpublish_on->value;
if (empty($unpublish_on) || $unpublish_on > $this->time
->getRequestTime()) {
continue;
}
// Do not process the node if it still has a publish_on time which is in
// the past, as this implies that scheduled publishing has been blocked
// by one of the hook functions we provide, and is still being blocked
// now that the unpublishing time has been reached.
$publish_on = $node->publish_on->value;
if (!empty($publish_on) && $publish_on <= $this->time
->getRequestTime()) {
continue;
}
// Check that other modules allow the action on this node.
if (!$this
->isAllowed($node, $action)) {
continue;
}
// $node->setChangedTime($unpublish_on) will fail badly if an API call
// has removed the date. Trap this as an exception here and give a
// meaningful message.
// @todo This will now never be thrown due to the empty(unpublish_on)
// check above to cater for translations. Remove this exception?
if (empty($unpublish_on)) {
$field_definitions = $this->entityTypeManager
->getFieldDefinitions('node', $node
->getType());
$field = (string) $field_definitions['unpublish_on']
->getLabel();
throw new SchedulerMissingDateException(sprintf("Node %d '%s' will not be unpublished because field '%s' has no value", $node
->id(), $node
->getTitle(), $field));
}
// Trigger the PRE_UNPUBLISH event so that modules can react before the
// node is unpublished.
$event = new SchedulerEvent($node);
$this
->dispatch($event, SchedulerEvents::PRE_UNPUBLISH);
$node = $event
->getNode();
// Update 'changed' timestamp.
$node
->setChangedTime($unpublish_on);
$create_unpublishing_revision = $node->type->entity
->getThirdPartySetting('scheduler', 'unpublish_revision', $this
->setting('default_unpublish_revision'));
if ($create_unpublishing_revision) {
$node
->setNewRevision();
// Use a core date format to guarantee a time is included.
$revision_log_message = $this
->t('Unpublished by Scheduler. The scheduled unpublishing date was @unpublish_on.', [
'@unpublish_on' => $this->dateFormatter
->format($unpublish_on, 'short'),
]);
// Create the new revision, setting message and revision timestamp.
$node
->setRevisionLogMessage($revision_log_message)
->setRevisionCreationTime($this->time
->getRequestTime());
}
// Unset unpublish_on so the node will not get rescheduled by subsequent
// calls to $node->save().
$node->unpublish_on->value = NULL;
// Invoke all implementations of hook_scheduler_unpublish_action() to
// allow other modules to do the "unpublishing" process instead of
// Scheduler.
$hook = 'scheduler_unpublish_action';
$processed = FALSE;
$failed = FALSE;
foreach ($this->moduleHandler
->getImplementations($hook) as $module) {
$function = $module . '_' . $hook;
$return = $function($node);
$processed = $processed || $return === 1;
$failed = $failed || $return === -1;
}
// Set up the log variables.
$view_link = $node
->toLink($this
->t('View node'));
$node_type = $this->entityTypeManager
->getStorage('node_type')
->load($node
->bundle());
$node_type_link = $node_type
->toLink($this
->t('@label settings', [
'@label' => $node_type
->label(),
]), 'edit-form');
$logger_variables = [
'@type' => $node_type
->label(),
'%title' => $node
->getTitle(),
'link' => $node_type_link
->toString() . ' ' . $view_link
->toString(),
'@hook' => 'hook_' . $hook,
];
if ($failed) {
// At least one hook function returned a failure or exception, so stop
// processing this node and move on to the next one.
$this->logger
->warning('Unpublishing failed for %title. Calls to @hook returned a failure code.', $logger_variables);
continue;
}
elseif ($processed) {
// The node has 'unpublishing' processed by a module implementing the
// hook, so no need to do anything more, apart from log this result.
$this->logger
->notice('@type: scheduled processing of %title completed by calls to @hook.', $logger_variables);
}
else {
// None of the above hook calls processed the node and there were no
// errors detected so set the node to unpublished.
$this->logger
->notice('@type: scheduled unpublishing of %title.', $logger_variables);
$node
->setUnpublished();
}
// Invoke event to tell Rules that Scheduler has unpublished this node.
if ($this->moduleHandler
->moduleExists('scheduler_rules_integration')) {
_scheduler_rules_integration_dispatch_cron_event($node, 'unpublish');
}
// Trigger the UNPUBLISH event so that modules can react after the node
// is unpublished.
$event = new SchedulerEvent($node);
$this
->dispatch($event, SchedulerEvents::UNPUBLISH);
// Use the standard actions system to unpublish and save the node.
$node = $event
->getNode();
$action_id = 'node_unpublish_action';
if ($this->moduleHandler
->moduleExists('workbench_moderation_actions')) {
// workbench_moderation_actions module uses a custom action instead.
$action_id = 'state_change__node__archived';
}
$this->entityTypeManager
->getStorage('action')
->load($action_id)
->getPlugin()
->execute($node);
$result = TRUE;
}
}
return $result;
}
/**
* Checks whether a scheduled action on a node is allowed.
*
* This provides a way for other modules to prevent scheduled publishing or
* unpublishing, by implementing hook_scheduler_allow_publishing() or
* hook_scheduler_allow_unpublishing().
*
* @param \Drupal\node\NodeInterface $node
* The node on which the action is to be performed.
* @param string $action
* The action that needs to be checked. Can be 'publish' or 'unpublish'.
*
* @return bool
* TRUE if the action is allowed, FALSE if not.
*
* @see hook_scheduler_allow_publishing()
* @see hook_scheduler_allow_unpublishing()
*/
public function isAllowed(NodeInterface $node, $action) {
// Default to TRUE.
$result = TRUE;
// Check that other modules allow the action.
$hook = 'scheduler_allow_' . $action . 'ing';
foreach ($this->moduleHandler
->getImplementations($hook) as $module) {
$function = $module . '_' . $hook;
$result &= $function($node);
}
return $result;
}
/**
* Gather node IDs for all nodes that need to be $action'ed.
*
* Modules can implement hook_scheduler_nid_list($action) and return an array
* of node ids which will be added to the existing list.
*
* @param string $action
* The action being performed, either "publish" or "unpublish".
*
* @return array
* An array of node ids.
*/
public function nidList($action) {
$nids = [];
foreach ($this->moduleHandler
->getImplementations('scheduler_nid_list') as $module) {
$function = $module . '_scheduler_nid_list';
$nids = array_merge($nids, $function($action));
}
return $nids;
}
/**
* Run the lightweight cron.
*
* The Scheduler part of the processing performed here is the same as in the
* normal Drupal cron run. The difference is that only scheduler_cron() is
* executed, no other modules hook_cron() functions are called.
*
* This function is called from the external crontab job via url
* /scheduler/cron/{access key} or it can be run interactively from the
* Scheduler configuration page at /admin/config/content/scheduler/cron.
* It is also executed when running Scheduler Cron via drush.
*
* @param array $options
* Options passed from drush command or admin form.
*/
public function runLightweightCron(array $options = []) {
// When calling via drush the log messages can be avoided by using --nolog.
$log = $this
->setting('log') && empty($options['nolog']);
if ($log) {
if (array_key_exists('nolog', $options)) {
$trigger = 'drush command';
}
elseif (array_key_exists('admin_form', $options)) {
$trigger = 'admin user form';
}
else {
$trigger = 'url';
}
$this->logger
->notice('Lightweight cron run activated by @trigger.', [
'@trigger' => $trigger,
]);
}
scheduler_cron();
if (ob_get_level() > 0) {
$handlers = ob_list_handlers();
if (isset($handlers[0]) && $handlers[0] == 'default output handler') {
ob_clean();
}
}
if ($log) {
$link = Link::fromTextAndUrl($this
->t('settings'), Url::fromRoute('scheduler.cron_form'));
$this->logger
->notice('Lightweight cron run completed.', [
'link' => $link
->toString(),
]);
}
}
/**
* Helper method to access the settings of this module.
*
* @param string $key
* The key of the configuration.
*
* @return \Drupal\Core\Config\ImmutableConfig
* The value of the configuration item requested.
*/
protected function setting($key) {
return $this->configFactory
->get('scheduler.settings')
->get($key);
}
/**
* Helper method to load latest revision of each node.
*
* @param array $nids
* Array of node ids.
*
* @return array
* Array of loaded nodes.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
protected function loadNodes(array $nids) {
$node_storage = $this->entityTypeManager
->getStorage('node');
$nodes = [];
// Load the latest revision for each node.
foreach ($nids as $nid) {
$node = $node_storage
->load($nid);
$revision_ids = $node_storage
->revisionIds($node);
$vid = end($revision_ids);
$nodes[] = $node_storage
->loadRevision($vid);
}
return $nodes;
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
SchedulerManager:: |
protected | property | Config Factory service object. | |
SchedulerManager:: |
protected | property | Date formatter service object. | |
SchedulerManager:: |
protected | property | Entity Type Manager service object. | |
SchedulerManager:: |
protected | property | The event dispatcher. | |
SchedulerManager:: |
protected | property | Scheduler Logger service object. | |
SchedulerManager:: |
protected | property | Module handler service object. | |
SchedulerManager:: |
protected | property | The time service. | |
SchedulerManager:: |
public | function | Dispatch a Scheduler event. | |
SchedulerManager:: |
public | function | Checks whether a scheduled action on a node is allowed. | |
SchedulerManager:: |
protected | function | Helper method to load latest revision of each node. | |
SchedulerManager:: |
public | function | Gather node IDs for all nodes that need to be $action'ed. | |
SchedulerManager:: |
public | function | Publish scheduled nodes. | |
SchedulerManager:: |
public | function | Run the lightweight cron. | |
SchedulerManager:: |
protected | function | Helper method to access the settings of this module. | |
SchedulerManager:: |
public | function | Unpublish scheduled nodes. | |
SchedulerManager:: |
public | function | Constructs a SchedulerManager object. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |