foldershare-8.x-1.2/src/Entity/FolderShareScheduledTask.php

src/Entity/FolderShareScheduledTask.php
<?php
 
namespace Drupal\foldershare\Entity;
 
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
 
use Drupal\foldershare\ManageLog;
use Drupal\foldershare\Entity\Exception\ValidationException;
use Drupal\foldershare\Utilities\LimitUtilities;
 
/**
 * Describes an internal task to be performed at a time in the future.
 *
 * The module's internal scheduled tasks are used to perform background
 * activity to finish long operations, such as copying, moving, or deleting
 * a large folder tree. Each task has:
 *
 * - A callback used to run the task.
 *
 * - An array of parameters, such as lists of entity IDs to copy, move,
 *   or delete.
 *
 * Each task has three dates/times:
 * - The date/time the task was first started.
 * - The date/time the current task object was created.
 * - The date/time the task is scheduled to run.
 *
 * When the task is first started, the creation date matches the start date.
 * The scheduled run date is a short time in the future. Each time the task
 * is run, it is removed from the database and its parameters passed to
 * the task callback. If a callback cannot finish the task within
 * execution and memory use limits, the callback creates a new task to continue
 * the work. The new task has the same original start date, a new creation
 * date, and a future scheduled date.
 *
 * For debugging and monitoring, each task also has:
 * - A brief text comment describing the task.
 * - A total execution time so far, measured in seconds.
 *
 * Like all entities, a task also has:
 * - An ID.
 * - A unique ID.
 * - A user ID for the user that started the task.
 *
 * The user ID is the current user when the task was first started. The task
 * will run by CRON or at the end of pages delivered to any user.
 * For those runs, the current user is whomever is running CRON or
 * whomever got the most recent page, but the user ID in the task remains the
 * ID of the original user that started the task. It is this original user
 * that owns any new content created by the task.
 *
 * <B>Task processing</B>
 * The list of scheduled tasks is processed in one of three ways:
 * - At the end of a request.
 * - When CRON runs.
 * - From a drush command.
 *
 * The module provides an event subscriber that listens for the "terminate"
 * event sent after a request has been finished. This is normally at the end
 * of every page delivered to a user, or after any REST or AJAX request.
 * The event subscriber calls this class's executeTasks() method.
 *
 * The module includes a CRON hook that is called each time CRON is invoked,
 * whether via an external CRON trigger or via a "terminate" event listened
 * to by the Automated Cron module. At this time, the hook calls this class's
 * executeTasks() method.
 *
 * The module includes drush commands to list tasks, delete tasks, and run
 * tasks. Drush can call this class's executeTasks() method.
 *
 * In any case, executeTasks() quickly checks if there are any tasks ready
 * to run, then runs them. A task is ready to run only if its scheduled time
 * is equal to the current time or in the past. If there are multiple tasks
 * ready to run, they are executed in order of their scheduled times.
 *
 * <B>Task visibility</B>
 * Tasks are strictly an internal implementation detail of this module.
 * They are not intended to be seen by users or administrators. For this
 * reason, the entity definition intentionally omits features:
 * - The entity type is marked as internal.
 * - None of the entity type's fields are viewable.
 * - None of the entity type's fields are editable.
 * - The entity type is not fieldable.
 * - The entity type has no "views_builder" to present view pages.
 * - The entity type has no "views_data" for creating views.
 * - The entity type has no "list_builder" to show lists.
 * - The entity type has no edit forms.
 * - The entity type has no access controller.
 * - The entity type has no admin permission.
 * - The entity type has no routes.
 * - The entity type has no caches.
 *
 * <B>Warning:</B> This class is strictly internal to the FolderShare
 * module. The class's existance, name, and content may change from
 * release to release without any promise of backwards compatability.
 *
 * @ingroup foldershare
 *
 * @see foldershare_cron()
 * @see \Drupal\foldershare\EventSubscriber\FolderShareScheduledTaskHandler
 *
 * @ContentEntityType(
 *   id               = "foldershare_scheduledtask",
 *   label            = @Translation("FolderShare internal scheduled task"),
 *   base_table       = "foldershare_scheduledtask",
 *   internal         = TRUE,
 *   persistent_cache = FALSE,
 *   render_cache     = FALSE,
 *   static_cache     = FALSE,
 *   fieldable        = FALSE,
 *   entity_keys      = {
 *     "id"           = "id",
 *     "uuid"         = "uuid",
 *     "label"        = "operation",
 *   },
 * )
 */
final class FolderShareScheduledTask extends ContentEntityBase {
 
  /*---------------------------------------------------------------------
   *
   * Fields.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Indicates if task execution is enabled.
   *
   * In normal use, this value is TRUE. However, if task execution needs
   * to be disabled FOR THE CURRENT PROCESS ONLY, this flag may be set
   * to FALSE. Future calls to executeTasks() will return doing nothing.
   *
   * This is primarily used by drush commands to disable task execution
   * during a command so that tasks don't interfer with whatever the command
   * is trying to do.
   *
   * @var bool
   *
   * @see ::executeTasks()
   * @see ::isTaskExecutionEnabled()
   * @see ::setTaskExecutionEnabled()
   */
  private static $enabled = TRUE;
 
  /*---------------------------------------------------------------------
   *
   * Constants - Entity type id.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * The entity type id for the FolderShare Scheduled Task entity.
   *
   * This is 'foldershare_scheduledtask' and it must match the entity type
   * declaration in this class's comment block.
   *
   * @var string
   */
  const ENTITY_TYPE_ID = 'foldershare_scheduledtask';
 
  /*---------------------------------------------------------------------
   *
   * Constants - Database tables.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * The base table for 'foldershare_scheduledtask' entities.
   *
   * This is 'foldershare_scheduledtask' and it must match the base table
   * declaration in this class's comment block.
   *
   * @var string
   */
  const BASE_TABLE = 'foldershare_scheduledtask';
 
  /*---------------------------------------------------------------------
   *
   * Entity definition.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Defines the fields used by instances of this class.
   *
   * The following fields are defined, along with their intended
   * public or private access:
   *
   * | Field            | Allow for view | Allow for edit |
   * | ---------------- | -------------- | -------------- |
   * | id               | no             | no             |
   * | uuid             | no             | no             |
   * | uid              | no             | no             |
   * | created          | no             | no             |
   * | operation        | no             | no             |
   * | parameters       | no             | no             |
   * | scheduled        | no             | no             |
   * | started          | no             | no             |
   * | comments         | no             | no             |
   * | executiontime    | no             | no             |
   *
   * Some fields are supported by parent class methods:
   *
   * | Field            | Get method                           |
   * | ---------------- | ------------------------------------ |
   * | id               | ContentEntityBase::id()              |
   * | uuid             | ContentEntityBase::uuid()            |
   * | operation        | ContentEntityBase::getName()         |
   *
   * Some fields are supported by methods in this class:
   *
   * | Field            | Get method                           |
   * | ---------------- | ------------------------------------ |
   * | uid              | getRequester()                       |
   * | created          | getCreatedTime()                     |
   * | operation        | getCallback()                        |
   * | parameters       | getParameters()                      |
   * | scheduled        | getScheduledTime()                   |
   * | started          | getStartedTime()                     |
   * | comments         | getComments()                        |
   * | executiontime    | getAccumulatedExecutionTime()        |
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entityType
   *   The entity type for which we are returning base field definitions.
   *
   * @return array
   *   An array of field definitions where keys are field names and
   *   values are BaseFieldDefinition objects.
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entityType) {
    //
    // Base class fields
    // -----------------
    // The parent ContentEntityBase class supports several standard
    // entity fields:
    //
    // - id: the entity ID
    // - uuid: the entity unique ID
    // - langcode: the content language
    // - revision: the revision ID
    // - bundle: the entity bundle
    //
    // The parent class ONLY defines these fields if they exist in
    // THIS class's comment block declaring class fields.  Of the
    // above fields, we only define these for this class:
    //
    // - id
    // - uuid
    //
    // By invoking the parent class, we don't have to define these
    // ourselves below.
    $fields = parent::baseFieldDefinitions($entityType);
 
    // Entity id.
    // This field was already defined by the parent class.
    $fields[$entityType->getKey('id')]
      ->setDescription(t('The ID of the task.'))
      ->setDisplayConfigurable('view', FALSE);
 
    // Unique id (UUID).
    // This field was already defined by the parent class.
    $fields[$entityType->getKey('uuid')]
      ->setDescription(t('The UUID of the task.'))
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    //
    // Common fields
    // -------------
    // Tasks have several fields describing the task, when it was started,
    // and when it should next run.
    //
    // Operation (callback).
    $fields['operation'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Callback'))
      ->setDescription(t('The task callback.'))
      ->setRequired(TRUE)
      ->setSettings([
        'default_value'   => '',
        'max_length'      => 256,
        'text_processing' => FALSE,
      ])
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Requester (original) user id.
    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Requester'))
      ->setDescription(t("The user ID that requested the task."))
      ->setRequired(TRUE)
      ->setSetting('target_type', 'user')
      ->setSetting('handler', 'default')
      ->setDefaultValueCallback(
        'Drupal\foldershare\Entity\FolderShareScheduledTask::getCurrentUserId')
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Creation date.
    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Created date'))
      ->setDescription(t('The date and time when this task entry was created.'))
      ->setRequired(TRUE)
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Scheduled time to run.
    $fields['scheduled'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('Scheduled time'))
      ->setDescription(t('The date and time when the task is scheduled to run.'))
      ->setRequired(TRUE)
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Task parameters.
    $fields['parameters'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Task parameters'))
      ->setDescription(t('The JSON parameters for the task.'))
      ->setRequired(FALSE)
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Original operation date.
    $fields['started'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('Original start date'))
      ->setDescription(t('The date and time when the operation started.'))
      ->setRequired(FALSE)
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Comments.
    $fields['comments'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Comments'))
      ->setDescription(t("The task's comments."))
      ->setRequired(FALSE)
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    // Accumulated run time.
    $fields['executiontime'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Accumulated execution time'))
      ->setDescription(t("The task's total execution time to date."))
      ->setRequired(FALSE)
      ->setDisplayConfigurable('view', FALSE)
      ->setDisplayConfigurable('form', FALSE);
 
    return $fields;
  }
 
  /*---------------------------------------------------------------------
   *
   * General utilities.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Returns the current user ID.
   *
   * This function provides the deault value callback for the 'uid'
   * base field definition.
   *
   * @return array
   *   An array of default values. In this case, the array only
   *   contains the current user ID.
   *
   * @see ::baseFieldDefinitions()
   */
  public static function getCurrentUserId() {
    return [\Drupal::currentUser()->id()];
  }
 
  /**
   * Validates a class and method callback.
   *
   * A standard PHP callback has one of these forms:
   * - String:
   *   - FUNCTIONNAME
   *   - CLASSNAME::METHODNAME
   *   - CLASSOBJECT::METHODNAME
   * - Array:
   *   - [CLASSNAME, METHODNAME]
   *   - [CLASSOBJECT, METHODNAME]
   *
   * FUNCTIONNAME, CLASSNAME, and METHODNAME must exist. The METHODNAME must
   * be a public static method.
   *
   * @param mixed $callback
   *   A string naming a function or a class and public static method, or
   *   an array with two values that include either the class name or an
   *   object instance, and a public static method name.
   *
   * @return string
   *   Returns a string of the form "FUNCTIONNAME" or "CLASSNAME::METHODNAME",
   *   where the function, class, and method have all been verified.
   *
   * @throws \Drupal\foldershare\Entity\Exception\ValidationException
   *   Throws an exception if the callback is malformed, if the function,
   *   class, or method does not exist, or if the method is not public
   *   and static.
   *
   * @see ::createTask()
   */
  private static function verifyCallback($callback) {
    // PHP's is_callable() checks for all valid forms of a callback,
    // including verifying that a function, class, or method exists.
    // It returns TRUE/FALSE and sets the callable name to a string
    // of the form "FUNCTIONNAME" or "CLASSNAME::METHODNAME".
    $callableName = '';
    if (is_callable($callback, FALSE, $callableName) === FALSE) {
      throw new ValidationException(t(
        'Malformed task callback is not in a recognized format or a class, method, or function does not exist.'));
    }
 
    // PHP's is_callable() does not check that the method is static and
    // public. PHP's call_user_func() handles non-static and/or non-public
    // methods with errors:
    // - If the method is private, PHP issues a warning.
    // - If the method is not static, PHP issues a fatal error and aborts.
    //
    // We'd rather not have these happen during a task, so we need to
    // validate that a named method is public and static. This requires
    // using the PHP reflection API.
    //
    // Split the callable name into CLASSNAME and METHODNAME, or just
    // FUNCTIONNAME. For the FUNCTIONNAME case, there is no further
    // checking required.
    $pieces = explode('::', $callableName);
    if (count($pieces) !== 2) {
      return $callableName;
    }
 
    $className = $pieces[0];
    $methodName = $pieces[1];
 
    // Get the reflection. Since is_callable() has already confirmed that
    // the class and method exist, this should not throw exceptions.
    try {
      $rClass = new \ReflectionClass($className);
      $rMethod = $rClass->getMethod($methodName);
 
      if ($rMethod->isStatic() === FALSE) {
        throw new ValidationException(t(
          'Invalid task callback method @method is not static.',
          [
            '@method' => $methodName,
          ]));
      }
 
      if ($rMethod->isPublic() === FALSE) {
        throw new ValidationException(t(
          'Invalid task callback method @method is not public.',
          [
            '@method' => $methodName,
          ]));
      }
    }
    catch (\Exception $e) {
      throw new ValidationException(t(
        'Malformed task callback class or method does not exist.'));
    }
 
    return $callableName;
  }
 
  /*---------------------------------------------------------------------
   *
   * Create.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Creates a new task.
   *
   * Every task has:
   * - A "class::method" string for the task callback.
   * - A set of parameters for that task (this may be empty).
   * - A user ID for the user that requested the task.
   * - A timestamp for the future time at which to run the task.
   * - A timestamp for when the operation was first started.
   * - Comments to describe the task during debugging.
   * - An accumulated run time, in seconds.
   *
   * Parameters must be appropriate for the task.
   *
   * This method should be used in preference to create() in order to
   * insure that the callback is valid and to use JSON encoding to process
   * task parameters to be saved with the task.
   *
   * @param int $timestamp
   *   The future time at which the task will be executed.
   * @param mixed $callback
   *   The callback for the task following standard PHP callable formats
   *   (e.g. "class::method" or [object, "method"], etc.).
   * @param int $requester
   *   (optional, default = (-1) = current user) The user ID of the
   *   individual causing the task to be created.
   * @param array $parameters
   *   (optional, default = NULL) An array of parameters to save with the
   *   task and pass to the task when it is executed.
   * @param int $started
   *   (optional, default = 0) The timestamp of the start date & time for
   *   an operation that causes a chain of tasks.
   * @param string $comments
   *   (optional, default = '') A comment on the current task.
   * @param int $executionTime
   *   (optional, default = 0) The accumulated total execution time of the
   *   task chain, in seconds.
   *
   * @return \Drupal\foldershare\Entity\FolderShareScheduledTask
   *   Returns the newly created task. The task will already have been saved
   *   to the task table.
   *
   * @throws \Drupal\foldershare\Entity\Exception\ValidationException
   *   Throws an exception if the callback is malformed or unrecognized, or
   *   if the the parameters array cannot be encoded as JSON.
   */
  public static function createTask(
    int $timestamp,
    $callback,
    int $requester = (-1),
    array $parameters = NULL,
    int $started = 0,
    string $comments = '',
    int $executionTime = 0) {
 
    // Validate the callback.
    switch ($callback) {
      // Well-known callbacks.
      case 'changeowner':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskChangeOwner';
        break;
 
      case 'copy-to-folder':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToFolder';
        break;
 
      case 'copy-to-root':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToRoot';
        break;
 
      case 'delete-hide':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskDelete1';
        break;
 
      case 'delete-delete':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskDelete2';
        break;
 
      case 'move-to-folder':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder';
        break;
 
      case 'move-to-root':
        $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot';
        break;
 
      case 'rebuildusage':
        $callback = '\Drupal\foldershare\ManageUsageStatistics::taskUpdateUsage';
        break;
    }
 
    // Throws an exception if the callback does not validate.
    $callback = self::verifyCallback($callback);
 
    // Insure we have a requester.
    if ($requester < 0) {
      $requester = (int) \Drupal::currentUser()->id();
    }
 
    // Convert parameters to a JSON encoding.
    if ($parameters === NULL) {
      $json = '';
    }
    else {
      $json = json_encode($parameters);
      if ($json === FALSE) {
        throw new ValidationException(t(
          'Task parameters cannot be JSON encoded for "@callback".',
          [
            '@callback' => $callback,
          ]));
      }
    }
 
    // Create the task. Let the ID, UUID, and creation date be automatically
    // assigned.
    $task = self::create([
      // Required fields.
      'operation'     => $callback,
      'uid'           => $requester,
      'scheduled'     => $timestamp,
 
      // Optional fields.
      'parameters'    => $json,
      'started'       => $started,
      'comments'      => $comments,
      'executiontime' => $executionTime,
    ]);
 
    $task->save();
 
    return $task;
  }
 
  /*---------------------------------------------------------------------
   *
   * Delete.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Deletes all tasks.
   *
   * Any task already executing will continue to execute until it finishes.
   * That execution may add new tasks, which will not be deleted.
   *
   * <B>Warning:</B> Deleting all tasks ends any pending operations, such as
   * those to delete, copy, or move content. This can leave these operations
   * in an indetermine state, with parts of the operation incomplete, locks
   * still locked, and entities marked hidden or disabled. This should only
   * be done as a last resort.
   *
   * @see ::deleteTask()
   * @see ::deleteTasks()
   * @see ::findNumberOfTasks()
   */
  public static function deleteAllTasks() {
    // Truncate the task table to delete everything.
    //
    // Since this entity type does not support a persistent cache, a static
    // cache, or a render cache, we do not have to worry about caches getting
    // out of sync with the database.
    $connection = Database::getConnection();
    $truncate = $connection->truncate(self::BASE_TABLE);
    $truncate->execute();
  }
 
  /**
   * Deletes a task.
   *
   * The task is deleted. If it is already executing, it will continue to
   * execute until it finishes. That execution may add new tasks, which
   * will not be deleted.
   *
   * <B>Warning:</B> Deleting a task ends any pending operation, such as one
   * to delete, copy, or move content. This can leave an operation in an
   * indetermine state, with parts of the operation incomplete, locks
   * still locked, and entities marked hidden or disabled. This should only
   * be done as a last resort.
   *
   * @param \Drupal\foldershare\Entity\FolderShareScheduledTask $task
   *   The task to delete.
   *
   * @see ::deleteAllTasks()
   * @see ::deleteTasks()
   * @see ::findNumberOfTasks()
   */
  public static function deleteTask(FolderShareScheduledTask $task) {
    if ($task === NULL) {
      return;
    }
 
    // It is possible that the task object has been loaded by more than one
    // process, then deleted by more than one process. The first delete
    // actually removes it from the entity table. The second delete does
    // nothing.
    try {
      $task->delete();
    }
    catch (\Exception $e) {
      // Do nothing.
    }
  }
 
  /**
   * Deletes tasks for a specific callback.
   *
   * All tasks with the indicated callback are deleted. If a task is
   * already executing, it will continue to execute until it finishes. That
   * execution may add new tasks, which will not be deleted.
   *
   * <B>Warning:</B> Deleting a task ends any pending operation, such as one
   * to delete, copy, or move content. This can leave an operation in an
   * indetermine state, with parts of the operation incomplete, locks
   * still locked, and entities marked hidden or disabled. This should only
   * be done as a last resort.
   *
   * @param mixed $callback
   *   (optional, default = '') The callback for the task following standard
   *   PHP callable formats (e.g. "class::method" or [object, "method"], etc.).
   *   If the callback is empty, all tasks are deleted.
   *
   * @see ::deleteAllTasks()
   * @see ::deleteTask()
   * @see ::findNumberOfTasks()
   */
  public static function deleteTasks($callback = '') {
    if (empty($callback) === TRUE) {
      self::deleteAllTasks();
    }
 
    // Throws an exception if the callback does not validate.
    $callback = self::verifyCallback($callback);
 
    // Delete all entries with the indicated callback.
    $connection = Database::getConnection();
    $query = $connection->delete(self::BASE_TABLE);
    $query->condition('operation', $callback, '=');
    $query->execute();
  }
 
  /*---------------------------------------------------------------------
   *
   * Fields access.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Returns the task's approximate accumulated execution time in seconds.
   *
   * A task may keep track of its accumulated execution time through a
   * chain of tasks, starting with the initial run of the task, followed
   * by a series of continuation runs.
   *
   * The execution time is approximate. If an operation schedules a
   * safety net task, runs for awhile, and is interrupted before it can
   * swap the safety net task with a continuation task, then the accumulated
   * execution time of the interrupted task will not have had a chance to
   * be saved into a continuation task.
   *
   * @return int
   *   Returns the approximate accumulated execution time in seconds.
   */
  public function getAccumulatedExecutionTime() {
    return $this->get('executiontime')->value;
  }
 
  /**
   * Returns the task's callback in the form "class::method".
   *
   * @return string
   *   Returns the name of the task callback.
   */
  public function getCallback() {
    return $this->get('operation')->value;
  }
 
  /**
   * Returns the task's optional comments.
   *
   * Comments are optional and may be used by a task to annotate why the
   * task exists or how it is progressing.
   *
   * @return string
   *   Returns the comments for the task.
   */
  public function getComments() {
    return $this->get('comments')->value;
  }
 
  /**
   * Returns the task's creation timestamp.
   *
   * @return int
   *   Returns the creation timestamp for this task.
   */
  public function getCreatedTime() {
    return $this->get('created')->value;
  }
 
  /**
   * Returns the task's parameters.
   *
   * @return array
   *   Returns an array of task parameters.
   */
  public function getParameters() {
    return $this->get('parameters')->value;
  }
 
  /**
   * Returns the user ID of the user that initiated the task.
   *
   * @return int
   *   Returns the requester's user ID.
   */
  public function getRequester() {
    return (int) $this->get('uid')->target_id;
  }
 
  /**
   * Returns the task's scheduled run timestamp.
   *
   * @return int
   *   Returns the scheduled run timestamp for this task.
   */
  public function getScheduledTime() {
    return $this->get('scheduled')->value;
  }
 
  /**
   * Returns the task's operation start timestamp.
   *
   * The start time is the time when an operation began, such as
   * the request time for a copy, move, or delete. This operation led to
   * the creation of the task object, which has created and scheduled times.
   * If that task reschedules itself into a continuing series of tasks,
   * all of them should share the same operation started timestamp.
   *
   * @return int
   *   Returns the original start timestamp for this task.
   */
  public function getStartedTime() {
    return $this->get('started')->value;
  }
 
  /*---------------------------------------------------------------------
   *
   * Find tasks.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Returns the number of scheduled tasks ready to be executed.
   *
   * Ready tasks are those with a task timestamp that is equal to
   * the current time or in the past.
   *
   * @param int $timestamp
   *   The timestamp used to select which tasks are ready.
   *
   * @see ::findNumberOfTasks()
   * @see ::findReadyTaskIds()
   * @see ::executeTasks()
   */
  public static function findNumberOfReadyTasks(int $timestamp) {
    $connection = Database::getConnection();
    $select = $connection->select(self::BASE_TABLE, "st");
    $select->condition('scheduled', $timestamp, '<=');
 
    return (int) $select->countQuery()->execute()->fetchField();
  }
 
  /**
   * Returns the number of scheduled tasks.
   *
   * If a callback name is provided, the returned number only counts
   * tasks with that name. If no name is given, all tasks are counted.
   *
   * @param string $callback
   *   (optional, default = '' = any) When set, returns the number of
   *   scheduled tasks with the given callback in the form "class::method".
   *   Otherwise returns the number of all scheduled tasks.
   *
   * @return int
   *   Returns the number of tasks.
   *
   * @see ::findNumberOfReadyTasks()
   * @see ::findReadyTaskIds()
   */
  public static function findNumberOfTasks(string $callback = '') {
    $connection = Database::getConnection();
    $select = $connection->select(self::BASE_TABLE, "st");
    if (empty($callback) === FALSE) {
      $select->condition('operation', $callback, '=');
    }
 
    return (int) $select->countQuery()->execute()->fetchField();
  }
 
  /**
   * Returns an ordered array of scheduled and ready task IDs.
   *
   * Ready tasks are those with a task timestamp that is equal to
   * the current time or in the past. The returned array is ordered
   * from oldest to newest.
   *
   * @param int $timestamp
   *   The timestamp used to select which tasks are ready.
   *
   * @return int[]
   *   Returns an array of task IDs for ready tasks, ordered from
   *   oldest to newest.
   *
   * @see ::findNumberOfReadyTasks()
   * @see ::findNumberOfTasks()
   * @see ::executeTasks()
   */
  public static function findReadyTaskIds(int $timestamp) {
    $connection = Database::getConnection();
    $select = $connection->select(self::BASE_TABLE, "st");
    $select->addField('st', 'id', 'id');
    $select->condition('scheduled', $timestamp, '<=');
    $select->orderBy('scheduled');
 
    return $select->execute()->fetchCol(0);
  }
 
  /**
   * Returns an ordered array of all scheduled task IDs.
   *
   * If a callback name is provided, the returned list only includes
   * tasks with that callback. If no callback is given, all tasks are included.
   *
   * The returned array is ordered from oldest to newest.
   *
   * @param string $callback
   *   (optional, default = '' = any) When set, returns the scheduled tasks
   *   with the given callback in the form "class::method". Otherwise returns
   *   all scheduled tasks.
   *
   * @return int[]
   *   Returns an array of task IDs, ordered from oldest to newest.
   *
   * @see ::findReadyTaskIds()
   * @see ::findNumberOfTasks()
   */
  public static function findTaskIds(string $callback = '') {
    $connection = Database::getConnection();
    $select = $connection->select(self::BASE_TABLE, "st");
    $select->addField('st', 'id', 'id');
    if (empty($callback) === FALSE) {
      $select->condition('operation', $callback, '=');
    }
 
    $select->orderBy('scheduled');
 
    return $select->execute()->fetchCol(0);
  }
 
  /*---------------------------------------------------------------------
   *
   * Execute tasks.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Returns TRUE if task execution is enabled, and FALSE otherwise.
   *
   * At the start of every process, this is TRUE and ready tasks will execute
   * each time executeTasks() is called.
   *
   * Task execution can be disabled FOR THE CURRENT PROCESS ONLY by calling
   * setTaskExecutionEnabled() with a FALSE argument.
   *
   * @return bool
   *   Returns TRUE if enabled.
   *
   * @see ::setTaskExecutionEnabled()
   * @see ::executeTasks()
   */
  public static function isTaskExecutionEnabled() {
    return self::$enabled;
  }
 
  /**
   * Enables or disables task execution.
   *
   * At the start of every process, this is TRUE and ready tasks will execute
   * each time executeTasks() is called.
   *
   * When set to FALSE, executeTasks() will return immediately, doing nothing.
   * This may be used to temporarily disable task execution FOR THE CURRENT
   * PROCESS ONLY. This has no effect on other processes or future processes.
   *
   * A common use of execution disabling is by drush, which needs to execute
   * commands without necessarily running pending tasks.
   *
   * @param bool $enable
   *   TRUE to enable, FALSE to disable.
   *
   * @see ::isTaskExecutionEnabled()
   * @see ::executeTasks()
   */
  public static function setTaskExecutionEnabled(bool $enable) {
    self::$enabled = $enable;
  }
 
  /**
   * Executes ready tasks.
   *
   * All tasks with scheduled times equal to or earlier than the given
   * time stamp will be considered ready to run and executed in oldest to
   * newest order. Tasks are deleted just before they are executed. Tasks
   * may add more tasks.
   *
   * Task execution will abort if execution has been disabled FOR THE
   * CURRENT PROCESS ONLY using setTaskExecutionEnabled().
   *
   * @param int $timestamp
   *   The time used to find all ready tasks. Any task scheduled to run at
   *   this time, or earlier, is considered ready and will be executed.
   *   This is typically set to the current time, or the time at which an
   *   HTTP request was made.
   *
   * @see ::findNumberOfReadyTasks()
   * @see ::findReadyTaskIds()
   * @see ::isTaskExecutionEnabled()
   * @see ::setTaskExecutionEnabled()
   */
  public static function executeTasks(int $timestamp) {
    // If execution is disabled, return immediately.
    if (self::$enabled === FALSE) {
      return;
    }
 
    // If execution time is already above the soft execution limit,
    // return immediately.
    if (LimitUtilities::aboveExecutionTimeLimit() === TRUE) {
      return;
    }
 
    //
    // Quick reject.
    // -------------
    // This function is called after every page is sent to a user. It is
    // essential that it quickly decide if there is anything to do.
    try {
      if (self::findNumberOfReadyTasks($timestamp) === 0) {
        // Nothing to do.
        return;
      }
    }
    catch (\Exception $e) {
      // Query failed?
      return;
    }
 
    //
    // Get ready tasks.
    // ----------------
    // A task is ready if the current time is greater than or equal to
    // its scheduled time.
    try {
      $taskIds = self::findReadyTaskIds($timestamp);
    }
    catch (\Exception $e) {
      // Query failed?
      return;
    }
 
    //
    // Execute tasks.
    // --------------
    // Loop through the tasks and execute them.
    //
    // If a task fails to load, it has already been deleted. It may have
    // been serviced by another execution of this same method running in
    // another process after delivering a page for another user.
    foreach ($taskIds as $taskId) {
      $task = self::load($taskId);
      if ($task === NULL) {
        continue;
      }
 
      // Delete the task to reduce the chance that another process will
      // try to service the same task at the same time. This can still
      // happen and tasks must be written to consider this.
      $task->delete();
 
      // Copy out the task's values.
      $callback      = $task->getCallback();
      $json          = $task->getParameters();
      $requester     = $task->getRequester();
      $comments      = $task->getComments();
      $started       = $task->getStartedTime();
      $executionTime = $task->getAccumulatedExecutionTime();
 
      unset($task);
 
      // Decode JSON-encoded task parameters.
      if (empty($json) === TRUE) {
        $parameters = [];
      }
      else {
        $parameters = json_decode($json, TRUE, 512, JSON_OBJECT_AS_ARRAY);
        if ($parameters === NULL) {
          // The parameters could not be decoded! This should not happen
          // since they were encoded using json_encode() during task creation.
          // There is nothing we can do with the task.
          ManageLog::error(
            "Programmer error: Missing or malformed parameters for '@callback' task.",
            [
              '@callback' => $callback,
            ]);
          continue;
        }
      }
 
      // Dispatch to the callback. The class and method were previously
      // validated when the task was created, so this call should not fail
      // with a PHP error. It still may fail due to a callback error.
      try {
        @call_user_func(
          $callback,
          $requester,
          $parameters,
          $started,
          $comments,
          $executionTime);
      }
      catch (\Exception $e) {
        // Unexpected exception.
        ManageLog::exception($e);
      }
 
      // If execution time is already above the soft execution limit,
      // stop processing tasks.
      if (LimitUtilities::aboveExecutionTimeLimit() === TRUE) {
        break;
      }
    }
  }
 
}

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

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