maestro-3.0.1-rc2/src/Engine/MaestroEngine.php

src/Engine/MaestroEngine.php
<?php

namespace Drupal\maestro\Engine;

// Used in the dynamic task call for task execution.
use Drupal\maestro\Engine\Exception\MaestroGeneralException;
use Drupal\maestro\Engine\Exception\MaestroSaveEntityException;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Component\Utility\Crypt;
use Drupal\user\Entity\User;
use Drupal\Core\Url;

/**
 * Class MaestroEngine.
 *
 * The base class for the v2 Maestro Engine
 * This class provides the base methods for the engine to act upon.
 *
 * API class for programmers is separate
 *
 * @ingroup maestro
 * @author randy
 */
class MaestroEngine {

  /**
   * Set debug to true/false to turn on/off debugging respectively.
   *
   * @var bool
   */
  protected $debug;


  /**
   * Set the development mode of the engine.
   *
   * True will turn on things like cache clearing for entity loads etc.
   *
   * @var bool
   */
  protected $developmentMode = FALSE;

  /**
   * The version of the engine.  Right now, pegged at 2.  D7's engine is v1.
   *
   * @var int
   */
  protected $version = 2;

  /**
   * Constructor.
   */
  public function __construct() {
    $this->debug = FALSE;
    $this->developmentMode = FALSE;
  }

  /*
   * The following methods are exposed as
   * the API for the engine.
   */

  /**
   * EnableDebug method.  Call this method to turn on debug.
   */
  public function enableDebug() {
    $this->debug = TRUE;
  }

  /**
   * DisableDebug.  Call this method to turn off debug.
   */
  public function disableDebug() {
    $this->debug = FALSE;
  }

  /**
   * GetDebug.  Returns TRUE or FALSE depending on whether debug is on or not.
   */
  public function getDebug() {
    return $this->debug;
  }

  /**
   * EnableDevelopmentMode method.  Call this method to turn on devel mode.
   */
  public function enableDevelopmentMode() {
    $this->developmentMode = TRUE;
  }

  /**
   * DisableDevelopmentMode.  Call this method to turn off devel mode.
   */
  public function disableDevelopmentMode() {
    $this->developmentMode = FALSE;
  }

  /**
   * GetDevelopmentMode.  Returns TRUE or FALSE depending on whether dev mode is on or not.
   */
  public function getDevelopmentMode() {
    return $this->developmentMode;
  }

  /**
   * GetTemplates method
   *  Gets all of the available Maestro templates established in the system.
   *
   * @return array
   *   Array or FALSE
   */
  public static function getTemplates() {
    $entity_store = \Drupal::entityTypeManager()->getStorage('maestro_template');
    return $entity_store->loadMultiple();
  }

  /**
   * GetTemplate
   *   Gets a specific Maestro Template.
   *
   * @param string $machine_name
   *   The machine name of a specific template you wish to fetch.
   *
   * @return array
   *   Array or FALSE
   */
  public static function getTemplate($machine_name) {
    $entity_store = \Drupal::entityTypeManager()->getStorage('maestro_template');
    $maestro_template = $entity_store->load($machine_name);
    return $maestro_template;
  }

  /**
   * GetTemplateTaskByID fetches the task from the config template.
   *
   * @param string $templateMachineName
   *   Machine name for the template.
   * @param string $taskID
   *   Maestro Task ID.
   *
   * @return array|NULL
   *   The template's task definition array on success or NULL on failure.
   */
  public static function getTemplateTaskByID($templateMachineName, $taskID) {
    if ($templateMachineName) {
      $template = self::getTemplate($templateMachineName);
      if ($template) {
        return $template->tasks[$taskID] ?? NULL;
      }
    }

    return FALSE;
  }

  /**
   * Returns the template task based on the task stored in the queue.
   *
   * @param int $queueID
   *   Maestro Queue ID.
   */
  public static function getTemplateTaskByQueueID($queueID) {
    return MaestroEngine::getTemplateTaskByID(MaestroEngine::getTemplateIdFromProcessId(MaestroEngine::getProcessIdFromQueueId($queueID)), MaestroEngine::getTaskIdFromQueueId($queueID));
  }

  /**
   * Get the template variables from the template.
   *
   * @param string $templateMachineName
   *   Maestro machine name for the template.
   *
   * @return array
   *   The template variables.
   */
  public static function getTemplateVariables($templateMachineName) {
    $template = self::getTemplate($templateMachineName);
    return $template->variables;
  }

  /**
   * Get the template's machine name from the process ID.
   *
   * @param int $processID
   *   Maestro Process ID.
   *
   * @return string
   *   Returns the template machine name or FALSE on error.
   */
  public static function getTemplateIdFromProcessId($processID) {
    $processRecord = FALSE;
    if ($processID) {
      $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->load($processID);
    }
    if ($processRecord) {
      return $processRecord->template_id->getString();
    }
    return FALSE;
  }

  /**
   * Saves/updates the task.
   *
   * @param string $templateMachineName
   *   The machine name of the template the task belongs to.
   * @param string $taskMachineName
   *   The machine name of the task itself.
   * @param array $task
   *   The task array representation loaded from the template and augmented as you see fit.
   */
  public static function saveTemplateTask($templateMachineName, $taskMachineName, array $task) {
    $returnValue = FALSE;
    $template = MaestroEngine::getTemplate($templateMachineName);
    $taskID = $task['id'];
    $template->tasks[$taskID] = $task;
    try {
      $returnValue = $template->save();
    }
    catch (EntityStorageException $e) {
      // Something went wrong.  Catching it.  We will return FALSE to signify that it hasn't saved.
      $returnValue = FALSE;
    }
    return $returnValue;
  }

  /**
   * Removes a template task.
   *
   * @param string $templateMachineName
   *   Machine name of the Maestro Template.
   * @param string $taskToRemove
   *   The machine name of the task to remove.
   */
  public static function removeTemplateTask($templateMachineName, $taskToRemove) {
    $returnValue = FALSE;
    $template = MaestroEngine::getTemplate($templateMachineName);
    $templateTask = MaestroEngine::getTemplateTaskByID($templateMachineName, $taskToRemove);
    $pointers = MaestroEngine::getTaskPointersFromTemplate($templateMachineName, $taskToRemove);

    $tasks = $template->tasks;
    unset($tasks[$taskToRemove]);
    // We also have to remove all of the pointers that point to this task.
    foreach ($pointers as $pointer) {
      $nextsteps = explode(',', $tasks[$pointer]['nextstep']);
      $key = array_search($taskToRemove, $nextsteps);
      unset($nextsteps[$key]);
      $tasks[$pointer]['nextstep'] = implode(',', $nextsteps);
    }

    $template->tasks = $tasks;
    try {
      $returnValue = $template->save();
    }
    catch (EntityStorageException $e) {
      // Something went wrong.  Catching it.  We will return FALSE to signify that it hasn't saved.
      $returnValue = FALSE;
    }
    return $returnValue;
  }

  /**
   * Determines which task(s) point to the task in question.
   *
   * @param string $templateMachineName
   *   The template of the task you wish to investigate.
   * @param string $taskMachineName
   *   The task you want to know WHO points to it.
   *
   * @return array
   *   Returns an array of resulting task machine names (IDs) or empty array.
   */
  public static function getTaskPointersFromTemplate($templateMachineName, $taskMachineName) {
    $template = MaestroEngine::getTemplate($templateMachineName);
    $pointers = [];
    foreach ($template->tasks as $task) {
      $nextSteps = explode(',', $task['nextstep']);
      $nextFalseSteps = explode(',', $task['nextfalsestep']);
      if (array_search($taskMachineName, $nextSteps) !== FALSE || array_search($taskMachineName, $nextFalseSteps) !== FALSE) {
        $pointers[] = $task['id'];
      }
    }
    return $pointers;
  }

  /**
   * Determines which FALSE IF task(s) branch(es) point to the task in question.
   *
   * @param string $templateMachineName
   *   The template of the task you wish to investigate.
   * @param string $taskMachineName
   *   The task you want to know WHO points to it.
   *
   * @return array
   *   Returns an array of resulting task machine names (IDs) or empty array.
   */
  public static function getTaskFalsePointersFromTemplate($templateMachineName, $taskMachineName) {
    $template = MaestroEngine::getTemplate($templateMachineName);
    $pointers = [];
    foreach ($template->tasks as $task) {
      $nextFalseSteps = explode(',', $task['nextfalsestep']);
      if (array_search($taskMachineName, $nextFalseSteps) !== FALSE) {
        $pointers[] = $task['id'];
      }
    }
    return $pointers;
  }

  /**
   * Determines which TRUE IDF task(s) branch(es) point to the task in question.
   *
   * @param string $templateMachineName
   *   The template of the task you wish to investigate.
   * @param string $taskMachineName
   *   The task you want to know WHO points to it.
   *
   * @return array
   *   Returns an array of resulting task machine names (IDs) or empty array.
   */
  public static function getTaskTruePointersFromTemplate($templateMachineName, $taskMachineName) {
    $template = MaestroEngine::getTemplate($templateMachineName);
    $pointers = [];
    foreach ($template->tasks as $task) {
      $nextSteps = explode(',', $task['nextstep']);
      if (array_search($taskMachineName, $nextSteps) !== FALSE) {
        $pointers[] = $task['id'];
      }
    }
    return $pointers;
  }

  /**
   * Get the template's machine name from the queue ID.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   *
   * @return string
   *   Returns the task machine name or FALSE on error.
   */
  public static function getTaskIdFromQueueId($queueID) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    if ($queueRecord) {
      return $queueRecord->task_id->getString();
    }
    return FALSE;
  }

  /**
   * Get the Queue Token from the Queue ID.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   *
   * @return string|bool
   *   Returns the Token string or FALSE on failure
   */
  public static function getTokenFromQueueId($queueID) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    if ($queueRecord) {
      return $queueRecord->token->getString();
    }
    return FALSE;
  }

  /**
   * Get the Queue ID from the Queue Token.
   *
   * @param string $token
   *   The Maestro Queue Token.
   *
   * @return int|bool
   *   Returns the Queue ID integer or FALSE on failure
   */
  public static function getQueueIdFromToken($token) {
    $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
    $query->condition('token', $token)
      ->accessCheck(FALSE);
    $entity_ids = $query->execute();
    $val = FALSE;
    if (count($entity_ids) > 0) {
      $val = current($entity_ids);
    }
    return $val;
  }

  /**
   * Get the process ID from the Queue ID.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   *
   * @return int|bool
   *   Returns the process ID integer or FALSE on failure
   */
  public static function getProcessIdFromQueueId($queueID) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    if ($queueRecord) {
      return $queueRecord->process_id->getString();
    }
    return FALSE;
  }

  /**
   * Fetch the variable's value if it exists.  Returns FALSE on not being able to find the variable.
   *
   * @param string $variableName
   *   The variable name you wish to fetch the value of.
   * @param int $processID
   *   The Maestro Process ID in which the variable exists.
   *
   * @return bool|mixed
   *   Returns a boolean (FALSE) on failure or the value of the variable.
   */
  public static function getProcessVariable($variableName, $processID) {
    $query = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->getQuery();
    $query->condition('process_id', $processID)
      ->accessCheck(FALSE)
      ->condition('variable_name', $variableName);
    $entity_ids = $query->execute();
    // We are expecting only 1 result... if any.
    $val = FALSE;
    if (count($entity_ids) > 0) {
      $entityID = current($entity_ids);
      \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->resetCache([$entityID]);
      $processVariableRecord = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->load($entityID);
      $val = $processVariableRecord->variable_value->getString();
    }
    return $val;
  }

  /**
   * Set a process variable's value.
   * Does a post-variable-set call to re-create any production assignments that may rely on this variable's value.
   *
   * @param string $variableName
   *   The variable machine name you wish to set.
   * @param string $variableValue
   *   The variable's value you wish to set.
   * @param int $processID
   *   The Maestro Process ID.
   */
  public static function setProcessVariable($variableName, $variableValue, $processID) {
    $query = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->getQuery();
    $query->condition('process_id', $processID)
      ->accessCheck(FALSE)
      ->condition('variable_name', $variableName);
    $varID = $query->execute();
    if (count($varID) > 0) {
      $varRecord = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->load(current($varID));
      $varRecord->set('variable_value', $variableValue);
      $varRecord->save();
      // TODO: handle an error condition on save here.
      // most likely trap the error and return false.
      // now we determine if there's any production assignments done on this variable
      // by_variable = 1, process_variable needs to match our variable.
      $query = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->getQuery();
      $query ->condition('process_variable', current($varID))
        ->accessCheck(FALSE)
        ->condition('by_variable', '1')
        ->condition('task_completed', '0');
      $entries = $query->execute();
      // we're going to remove these entries and actually do a production assignment.
      $engine = new MaestroEngine();
      $storeAssignmentInfo = [];
      foreach ($entries as $assignmentID) {
        $assignRecord = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->load($assignmentID);
        $queueID = $assignRecord->queue_id->getString();
        $processID = MaestroEngine::getProcessIdFromQueueId($queueID);
        $taskID = MaestroEngine::getTaskIdFromQueueId($queueID);
        $templateMachineName = MaestroEngine::getTemplateIdFromProcessId($processID);
        // We store the assignment keyed on queueID because an assignment may have multiple asignees listed in the production assignments.
        $storeAssignmentInfo[$queueID] = [
          'templateMachineName' => $templateMachineName,
          'taskID' => $taskID,
        ];
        // Now remove this assignment.
        $assignRecord->delete();
      }
      foreach ($storeAssignmentInfo as $queueID => $assingmentInfo) {
        $engine->productionAssignments($assingmentInfo['templateMachineName'], $assingmentInfo['taskID'], $queueID);
      }

      // Now lets call our hooks on a post-save variable change
      // our built-in hook will update the production assignments if they exist.
      \Drupal::moduleHandler()->invokeAll('maestro_post_variable_save',
          [$variableName, $variableValue, $processID]);
    }
  }

  /**
   * Fetch the variable's unique ID from the variables table if it exists.  Returns FALSE on not being able to find the variable.
   *
   * @param string $variableName
   *   The variable's machine name you wish to retrieve the Unique ID for.
   * @param int $processID
   *   The Maestro Process ID.
   *
   * @return bool|mixed
   *   Returns a FALSE on failure or the variable's ID on success.
   */
  public static function getProcessVariableID($variableName, $processID) {
    $query = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->getQuery();
    $query->condition('process_id', $processID)
      ->accessCheck(FALSE)
      ->condition('variable_name', $variableName);
    $entity_ids = $query->execute();
    // We are expecting only 1 result... if any.
    $val = FALSE;
    if (count($entity_ids) > 0) {
      $entityID = current($entity_ids);
      $varRecord = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->resetCache([$entityID]);
      $varRecord = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->load($entityID);
      $val = $varRecord->id->getString();
    }
    return $val;
  }

  /**
   * SetProductionTaskLabel
   * Lets you set the task label for an in-production task.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   * @param string $taskLabel
   *   The string you wish to set the task's label to.
   */
  public static function setProductionTaskLabel($queueID, $taskLabel) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    $queueRecord->set('task_label', $taskLabel);
    $queueRecord->save();
  }

  /**
   * CompleteTask
   * Completes the queue record by setting the status bit to true/1.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   * @param int $userID
   *   The optional userID of the individual who completed this task.
   */
  public static function completeTask($queueID, $userID = 0) {
    $task_completion_time = time();
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    $queueRecord->set('status', TASK_STATUS_SUCCESS);
    $queueRecord->set('uid', $userID);
    $queueRecord->set('completed', $task_completion_time);
    $queueRecord->save();
    // Set the flag in the production assignments if it exists.
    $query = \Drupal::entityQuery('maestro_production_assignments')
      ->accessCheck(FALSE);
    $query->condition('queue_id', $queueID);
    $assignmentIDs = $query->execute();
    foreach ($assignmentIDs as $assignmentID) {
      $assignmentRecord = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->load($assignmentID);
      $assignmentRecord->set('task_completed', 1);
      $assignmentRecord->save();
    }

    // If this task participates in setting of process status, set the completion time.
    $task = MaestroEngine::getTemplateTaskByQueueID($queueID);
    if (isset($task['participate_in_workflow_status_stage']) && $task['participate_in_workflow_status_stage'] == 1) {
      // Query the maestro_process_status for the state entry.
      $process_id = MaestroEngine::getProcessIdFromQueueId($queueID);
      $query = \Drupal::entityQuery('maestro_process_status')
        ->condition('process_id', $process_id)
        ->condition('stage_number', $task['workflow_status_stage_number'])
        ->accessCheck(FALSE);
      $statusEntityIDs = $query->execute();
      foreach ($statusEntityIDs as $entity_id) {
        $statusRecord = \Drupal::entityTypeManager()->getStorage('maestro_process_status')->load($entity_id);
        if ($statusRecord) {
          $statusRecord->set('completed', $task_completion_time);
          $statusRecord->save();
        }
      }
    }
  }

  /**
   * SetTaskStatus
   * Sets a task's status.  Default is success.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   * @param int $status
   *   The status for this task (see defines in .module file)
   */
  public static function setTaskStatus($queueID, $status = TASK_STATUS_SUCCESS) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    $queueRecord->set('status', $status);
    $queueRecord->save();
  }

  /**
   * ArchiveTask
   * Archives the queue record by setting the archive bit to true/1.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   */
  public static function archiveTask($queueID) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    $queueRecord->set('archived', 1);
    $queueRecord->save();
  }

  /**
   * UnArchiveTask
   * The opposite of archiveTask.  Used in management of a flow.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   */
  public static function unArchiveTask($queueID) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    $queueRecord->set('archived', 0);
    $queueRecord->save();
  }

  /**
   * This static method will load the maestro_process entity identified by the processID param and will flag it as complete.
   *
   * @param int $processID
   *   The Maestro Process ID.
   */
  public static function endProcess($processID) {
    $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->load($processID);
    $processRecord->set('complete', PROCESS_STATUS_COMPLETED);
    $processRecord->set('completed', time());
    $processRecord->save();
  }

  /**
   * This static method will load the maestro_process entity identified by the processID param and will flag it as aborted.
   *
   * @param int $processID
   *   The Maestro Process ID.
   */
  public static function abortProcess($processID) {
    $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->load($processID);
    $processRecord->set('complete', PROCESS_STATUS_ABORTED);
    $processRecord->set('completed', time());
    $processRecord->save();
  }

  /**
   * This static method will load the maestro_process entity identified by the processID param and will set the process' label.
   *
   * @param int $processID
   *   The Maestro Process ID.
   * @param string $processLabel
   *   The label you wish to set the Process to.
   */
  public static function setProcessLabel($processID, $processLabel) {
    if ($processLabel) {
      $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->load($processID);
      $processRecord->set('process_name', $processLabel);
      $processRecord->save();
    }
  }

  /**
   * Fetch a user's assigned tasks.  Maestro base functionality will determine user and role assignments.
   * You can implement hook_maestro_post_fetch_assigned_queue_tasks(int userID, array &$entity_ids)
   * to fetch your own custom assignments whether that be by OG or some other assignment method.
   *
   * @param int $userID
   *   The numeric (integer) user ID for the user you wish to get their tasks for.
   *
   * @return array
   *   An array of entity IDs if they exist.  These entity IDs are the IDs from the maestro_queue table
   */
  public static function getAssignedTaskQueueIds($userID) {
    /*
     * Assignments by variable are done in the assignment table identical to that
     * of the regular user or role assignment, however, the only difference is that
     * there is an entity reference to the variable's ID and the by_variable flag
     * is set.  Therefore we can simply look for role and user assignments with
     * this query and return that as the queue IDs assigned to the user.
     *
     * The state of the task item needs to be checked to ensure that it has not been completed.
     */

    \Drupal::entityTypeManager()->getViewBuilder('maestro_production_assignments')->resetCache();
    $account = User::load($userID);
    $userRoles = $account->getRoles(TRUE);

    $query = \Drupal::entityQuery('maestro_production_assignments')
      ->accessCheck(FALSE);

    $andConditionByUserID = $query->andConditionGroup()
      ->condition('assign_id', $account->getAccountName())
      ->condition('assign_type', 'user');

    $orConditionAssignID = $query->orConditionGroup()
      ->condition($andConditionByUserID);

    $roleAND = NULL;
    foreach ($userRoles as $userRole) {
      $roleAND = $query->andConditionGroup()
        ->condition('assign_id', $userRole)
        ->condition('assign_type', 'role');
      $orConditionAssignID
        ->condition($roleAND);
    }

    $query->condition($orConditionAssignID);
    $query->condition('task_completed', '0');
    $query->condition('queue_id.entity.process_id.entity.complete', '0');
    $query->condition('queue_id.entity.archived', '0');
    $query->condition('queue_id.entity.status', '0');

    $assignmentIDs = $query->execute();
    $queueIDs = [];
    foreach ($assignmentIDs as $entity_id) {
      $assignmentRecord = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->load($entity_id);
      $queueIDs[] = $assignmentRecord->queue_id->getString();
    }

    // Now to invoke other modules to add their entity IDs to the already fetched list
    // pass to the invoked handler, the user ID and the current set of entity IDs.
    \Drupal::moduleHandler()->invokeAll('maestro_post_fetch_assigned_queue_tasks', [$userID, &$queueIDs]);
    return $queueIDs;
  }

  /**
   * Returns the queue entity record based on the queueID.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   */
  public static function getQueueEntryById($queueID) {
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->resetCache([$queueID]);
    $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
    return $queueRecord;
  }

  /**
   * Returns the process entity record based on the processID.
   *
   * @param int $processID
   *   The Maestro Process ID.
   */
  public static function getProcessEntryById($processID) {
    $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->resetCache([$processID]);
    $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->load($processID);
    return $processRecord;
  }

  /**
   * Returns the $task variable populated with a Maestro Task plugin.
   * See the getExecutableFormFields method in MaestroInteractiveFormBase class to see how you can fetch and use a task.
   *
   * @param string $taskClassName
   *   The task's class name.
   * @param int $processID
   *   Optional.
   * @param int $queueID
   *   Optional.
   *
   * @return mixed|NULL $task The task is returned in the $task variable.  This is one of the plugins defined in Drupal\maestro\Plugin\EngineTasks
   */
  public static function getPluginTask($taskClassName, $processID = 0, $queueID = 0) {
    $manager = \Drupal::service('plugin.manager.maestro_tasks');
    $plugins = $manager->getDefinitions();
    $task = NULL;
    // Ensure that this task type exists.
    if (array_key_exists($taskClassName, $plugins)) {
      $task = $manager->createInstance($plugins[$taskClassName]['id'], [$processID, $queueID]);
    }
    else {
      \Drupal::logger('maestro')->error('Plugin for task %task does not exist', ['%task' => $taskClassName]);
    }
    return $task;
  }

  /**
   * Determines if a user is assigned to execute a specific task.
   *
   * @param int $queueID
   *   The Maestro Queue ID.
   * @param int $userID
   *   The user ID you wish to determine if they can execute the task identified by the Queue ID.
   */
  public static function canUserExecuteTask($queueID, $userID) {
    $queueIDs = self::getAssignedTaskQueueIds($userID);
    $returnValue = FALSE;
    if (array_search($queueID, $queueIDs) !== FALSE) {
      $returnValue = TRUE;
    }
    /*
     * In the event we have a use case where there is no userID (e.g.: known-anonymous-user scenario)
     * we will need to provide a mechanism for Developers to validate a user.
     * Using an ALTER here, we let the developer override the existing value if needed.
     */
    \Drupal::moduleHandler()->alter('maestro_can_user_execute_task', $returnValue, $queueID, $userID);
    return $returnValue;
  }

  /**
   * Returns the assignment records as an associative array (if specified as such), or keyed array with the assigned ID as the
   * key and the assignment as a string, for the specific Queue item.
   *
   * Format of the associative array is.
   *
   * [Assigned ID][
   *   'assign_id' => the assigned ID (username, role name etc),
   *   'by_variable' => the assignment type if by variable or not (fixed or variable),
   *   'assign_type' => the assignment type,
   *   'id' => the ID of the assignment record
   *   ]
   *
   * Alternatively the structure of the keyed single entry array is:
   * [Assigned ID (username, role name, etc.)] => "The Assigned ID":"fixed or variable"
   *
   * @param string $queueID
   *   The queue ID for the fetch to operate on.
   * @param bool $associativeArray
   *   Set to TRUE to have an associative array returned.  FALSE for simple keyed array on the asignees.
   */
  public static function getAssignedNamesOfQueueItem($queueID, $associativeArray = FALSE) {
    $output = [];
    // Lets get the assignments based on this queue ID.
    $query = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->getQuery();
    $query->accessCheck(FALSE);
    $query->condition('queue_id', $queueID);
    $entity_ids = $query->execute();
    if (count($entity_ids) > 0) {
      foreach ($entity_ids as $assignmentID) {
        $assignRecord = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->load($assignmentID);
        $assignRecord->by_variable->getString() == 0 ? $type = t('Fixed') : $type = t('Variable');
        if ($associativeArray) {
          $output[$assignRecord->assign_id->getString()] = [
            'assign_id' => $assignRecord->assign_id->getString(),
            'by_variable' => $type,
            'assign_type' => $assignRecord->assign_type->getString(),
            'id' => $assignRecord->id->getString(),
          ];
        }
        else {
          // technically, you should only have one assignment record per entity here.  So shouldn't have to
          // worry about having the same username or role or group etc. named twice and having the array overwritten.
          $output[$assignRecord->assign_id->getString()] = $assignRecord->assign_id->getString() . ':' . $type;
        }
      }
    }

    return $output;
  }

  /**
   * Fetches a translatable label for task status.
   *
   * Please see maestro.module for TASK_STATUS defines.
   *
   * @param int $status
   *   The task's status integer value.
   */
  public static function getTaskStatusLabel($status) {
    $return_status = '';
    switch ($status) {
      case TASK_STATUS_ACTIVE:
        $return_status = t('Active');

        break;

      case TASK_STATUS_ABORTED:
        $return_status = t('Aborted');

        break;

      case TASK_STATUS_CANCEL:
        $return_status = t('Cancelled');

        break;

      case TASK_STATUS_HOLD:
        $return_status = t('On Hold');

        break;

      case TASK_STATUS_SUCCESS:
        $return_status = t('Successfully Completed');

        break;

      case TASK_STATUS_FALSE_BRANCH:
        $return_status = t('False Branch Completed');
        break;

      default:
        $return_status = t('Unknown');
        break;
    }
    return $return_status;
  }

  /**
   * Fetches the task status values in an associative array where the key
   * is the status ID and the value is the translated string.
   */
  public static function getTaskStatusArray() {
    $arr = [];
    $arr[TASK_STATUS_ACTIVE] = t('Active');
    $arr[TASK_STATUS_SUCCESS] = t('Successfully Completed');
    $arr[TASK_STATUS_CANCEL] = t('Cancelled');
    $arr[TASK_STATUS_HOLD] = t('On Hold');
    $arr[TASK_STATUS_ABORTED] = t('Aborted');
    $arr[TASK_STATUS_FALSE_BRANCH] = t('False Branch Completed');
    return $arr;
  }

  /**
   * Fetches the task archival status values in an associative array where the key
   * is the archival status ID and the value is the translated string.
   */
  public static function getTaskArchiveArray() {
    $arr = [];
    $arr[TASK_ARCHIVE_ACTIVE] = t('Active');
    $arr[TASK_ARCHIVE_NORMAL] = t('Archived');
    $arr[TASK_ARCHIVE_REGEN] = t('Regenerated');
    return $arr;
  }

  /**
   * Checks the validity of the template in question.
   *
   * @param string $templateMachineName
   *   The machine name of the template.
   *
   * @return array
   *   Will return an array with keys 'failures' and 'information' with each array contains an array
   *   in the form of ['taskID', 'taskLabel', 'reason'] that fail or create information during the validity check.
   *   Empty 'failures' key array if no issues.
   */
  public static function performTemplateValidityCheck($templateMachineName) {
    $template = MaestroEngine::getTemplate($templateMachineName);
    $pointers = [];
    $endTaskExists = FALSE;
    $validation_failure_tasks = [];
    $validation_information_tasks = [];
    foreach ($template->tasks as $task) {
      // Now determine who points to this task.  If this is an AND or an OR task, you can have multiple pointers.
      // if you're any other type of task, you've broken the validity of the template.
      $pointers = MaestroEngine::getTaskPointersFromTemplate($templateMachineName, $task['id']);
      // $task['tasktype'] holds the task type.
      // this is validation information, not an error.
      if ($task['tasktype'] !== 'MaestroOr' && $task['tasktype'] !== 'MaestroAnd') {
        // We have a non-logical validation failure here.
        if (count($pointers) > 1) {
          $validation_information_tasks[] = [
            'taskID' => $task['id'],
            'taskLabel' => $task['label'],
            'reason' => t('This task, with two pointers to it, will cause a regeneration to happen.  Please see documentation for more information.'),
          ];
        }
      }

      // Now check to see if the task has at least ONE pointer to it OTHER THAN THE START TASK!
      if ($task['tasktype'] !== 'MaestroStart') {
        // No pointers to this task.
        if (count($pointers) == 0) {
          $validation_failure_tasks[] = [
            'taskID' => $task['id'],
            'taskLabel' => $task['label'],
            'reason' => t('Task has no other tasks pointing to it. Only the Start Task is allowed to have no tasks pointing to it.'),
          ];
        }
      }
      if ($task['tasktype'] === 'MaestroEnd') {
        $endTaskExists = TRUE;
      }

      // Now let the task itself determine if it should fail the validation.
      $executable_task = NULL;
      $executable_task = MaestroEngine::getPluginTask($task['tasktype']);
      if ($executable_task != NULL) {
        $executable_task->performValidityCheck($validation_failure_tasks, $validation_information_tasks, $task);
      }

    }
    // Now we check to see if an end task exists...
    if (!$endTaskExists) {
      $validation_failure_tasks[] = [
        'taskID' => t('No Task ID'),
        'taskLabel' => t('No Task Label'),
        'reason' => t('This template is missing an END Task.  Without an END Task, the process will never be flagged as complete.'),
      ];
    }

    // Anyone else would like to add to or remove from the validation failure list here?  by all means:
    \Drupal::moduleHandler()->invokeAll('maestro_template_validation_check',
        [$templateMachineName, &$validation_failure_tasks, &$validation_information_tasks]);
    // If we have no validation issues, lets set the template to have its validity set to true.
    if (count($validation_failure_tasks) == 0) {
      $template->validated = TRUE;
    }
    else {
      $template->validated = FALSE;
    }
    $template->save();
    return [
      'failures' => $validation_failure_tasks,
      'information' => $validation_information_tasks,
    ];

  }

  /**
   * Sets a template to unvalidated based on machine name.
   *
   * @param string $templateMachineName
   *   The machine name of the template.
   */
  public static function setTemplateToUnvalidated($templateMachineName) {
    $template = MaestroEngine::getTemplate($templateMachineName);
    if ($template !== NULL) {
      $template->validated = FALSE;
      $template->save();
    }
  }

  /**
   * Creates a maestro_entity_identifier entity entry.
   *
   * @param int $processID
   *   The Maestro Process ID.
   * @param string $entityType
   *   The type of entity you are saving.
   * @param string $entityBundle
   *   The bundle of the entity.
   * @param string $taskUniqueID
   *   The unique ID you are associating to the entity identifier.
   * @param string|int $entityID
   *   The ID of the entity.
   *
   * @return int|string|null
   *   The row ID (entity ID) of the newly created maestro_entity_identifiers if successful.  NULL if not successful.
   */
  public static function createEntityIdentifier($processID, $entityType, $entityBundle, $taskUniqueID, $entityID) {
    if (isset($processID) && isset($entityType) && isset($entityBundle) && isset($taskUniqueID) && isset($entityID)) {
      $values = [
        'process_id' => $processID,
        'unique_id' => $taskUniqueID,
        'entity_type' => $entityType,
        'entity_id' => $entityID,
        'bundle' => $entityBundle,
      ];
      $new_entry = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->create($values);
      $new_entry->save();

      return $new_entry->id();
    }
    return NULL;
  }

  /**
   * Updates an existing maestro_entity_identifiers entity using the maestro_entity_identifiers table ID as the key.
   *
   * @param int $entityIdentifierID
   *   The entity identifier ID.
   * @param string $entityID
   *   The entity ID.
   * @param string $entityType
   *   The type of entity.
   * @param string $entityBundle
   *   The entity bundle.
   * @param string $taskUniqueID
   *   The unique identifier associated to the entity.
   */
  public static function updateEntityIdentifierByEntityTableID($entityIdentifierID, $entityID = NULL, $entityType = NULL, $entityBundle = NULL, $taskUniqueID = NULL) {
    if (isset($entityIdentifierID)) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->load($entityIdentifierID);
      if ($record) {
        if (isset($taskUniqueID)) {
          $record->set('unique_id', $taskUniqueID);
        }
        if (isset($entityType)) {
          $record->set('entity_type', $entityType);
        }
        if (isset($entityID)) {
          $record->set('entity_id', $entityID);
        }
        if (isset($entityBundle)) {
          $record->set('bundle', $entityBundle);
        }
        $record->save();
      }
    }
  }

  /**
   * Fetches the entity identifier (entity_id field) using the process ID and the unique ID given to the entity from the Maestro task.
   * Technically, there should only be one entity identifier for the uniqueID.
   *
   * @param int $processID
   *   The process ID from the workflow.
   * @param string $taskUniqueID
   *   The unique identifier given to the entity in the task definition.
   *
   * @return string
   *   The value of the entity_id field.
   */
  public static function getEntityIdentiferByUniqueID($processID, $taskUniqueID) {
    $value = NULL;

    $query = \Drupal::entityQuery('maestro_entity_identifiers')
      ->condition('process_id', $processID)
      ->condition('unique_id', $taskUniqueID)
      ->accessCheck(FALSE);
    $entityID = current($query->execute());
    if ($entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->load($entityID);
      if ($record) {
        $value = $record->entity_id->getString();
      }
    }
    return $value;
  }

  /**
   * Fetches the full record of the maestro_entity_identifiers entity for the process and unique ID.
   *
   * @param int $processID
   *   The process ID from the workflow.
   * @param string $taskUniqueID
   *   The unique identifier given to the entity in the task definition.
   *
   * @return array
   *   An array of arrays keyed by the task's unique identifier for the entity. Empty array if nothing found.
   */
  public static function getEntityIdentiferFieldsByUniqueID($processID, $taskUniqueID) {
    $retArray = [];
    $query = \Drupal::entityQuery('maestro_entity_identifiers')
      ->condition('process_id', $processID)
      ->condition('unique_id', $taskUniqueID)
      ->accessCheck(FALSE);
    $entityIDs = $query->execute();
    foreach ($entityIDs as $entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->load($entityID);
      if ($record) {
        $retArray[$record->unique_id->getString()] = [
          'unique_id' => $record->unique_id->getString(),
          'entity_type' => $record->entity_type->getString(),
          'bundle' => $record->bundle->getString(),
          'entity_id' => $record->entity_id->getString(),
        ];
      }
    }

    return $retArray;
  }

  /**
   * Fetches the entity identifier (entity_id field) using the maestro_entity_identifiers "id" column.
   *
   * @param int $rowID
   *   The ID column of the entity identifier entity table.
   *
   * @return string
   *   The value of the entity_id field.
   */
  public static function getEntityIdentiferByIdentifierRowID($rowID) {
    $value = NULL;

    $query = \Drupal::entityQuery('maestro_entity_identifiers')
      ->condition('id', $rowID)
      ->accessCheck(FALSE);
    $entityID = current($query->execute());
    $record = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->load($entityID);
    if ($record) {
      $value = $record->entity_id->getString();
    }

    return $value;
  }

  /**
   * Fetches all of the the entity identifiers in the maestro_entity_identifiers entity for the process.
   *
   * @param int $processID
   *   The Maestro Process ID.
   *
   * @return array
   *   An array of arrays keyed by the task's unique identifier for the entity. Empty array if nothing found.
   */
  public static function getAllEntityIdentifiersForProcess($processID) {
    $retArray = [];
    $query = \Drupal::entityQuery('maestro_entity_identifiers')
      ->condition('process_id', $processID)
      ->accessCheck(FALSE);
    $entityIDs = $query->execute();
    foreach ($entityIDs as $entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->load($entityID);
      if ($record) {
        $retArray[$record->unique_id->getString()] = [
          'unique_id' => $record->unique_id->getString(),
          'entity_type' => $record->entity_type->getString(),
          'bundle' => $record->bundle->getString(),
          'entity_id' => $record->entity_id->getString(),
        ];
      }
    }

    return $retArray;
  }

  /**
   * Fetches all of the the status entries for the process.
   *
   * @param int $processID
   *   The Maestro Process ID.
   *
   * @return array
   *   An array of arrays keyed by the stage number for the message. Empty array if nothing found.
   */
  public static function getAllStatusEntriesForProcess($processID) {
    $retArray = [];
    $query = \Drupal::entityQuery('maestro_process_status')
      ->condition('process_id', $processID)
      ->sort('stage_number', 'ASC')
      ->accessCheck(FALSE);
    $entityIDs = $query->execute();
    foreach ($entityIDs as $entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_process_status')->load($entityID);
      if ($record) {
        $retArray[$record->stage_number->getString()] = [
          'message' => $record->stage_message->getString(),
          'completed' => $record->completed->getString(),
          'stage_number' => $record->stage_number->getString(),
        ];
      }
    }

    return $retArray;
  }

  /**
   * Removes all data elements associated with a process. This includes queue, assignment, status, entity identifiers and variables.
   *
   * @param int $processID
   *   The Maestro Process ID.
   */
  public static function deleteProcess($processID) {
    // Delete queue items.
    $query = \Drupal::entityQuery('maestro_queue')
      ->condition('process_id', $processID)
      ->accessCheck(FALSE);
    $ids = $query->execute();
    foreach ($ids as $queueID) {
      // Delete the assignments.
      $query = \Drupal::entityQuery('maestro_production_assignments')
        ->condition('queue_id', $queueID)
        ->accessCheck(FALSE);
      $entityIDs = $query->execute();
      foreach ($entityIDs as $entityID) {
        $record = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->load($entityID);
        if ($record) {
          $record->delete();
        }
      }
      $queueRecord = MaestroEngine::getQueueEntryById($queueID);
      $queueRecord->delete();
    }

    // Delete entity identifiers.
    $query = \Drupal::entityQuery('maestro_entity_identifiers')
      ->condition('process_id', $processID)
      ->accessCheck(FALSE);
    $entityIDs = $query->execute();
    foreach ($entityIDs as $entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_entity_identifiers')->load($entityID);
      if ($record) {
        $record->delete();
      }
    }

    // Delete process variables.
    $query = \Drupal::entityQuery('maestro_process_variables')
      ->condition('process_id', $processID)
      ->accessCheck(FALSE);
    $entityIDs = $query->execute();
    foreach ($entityIDs as $entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->load($entityID);
      if ($record) {
        $record->delete();
      }
    }

    // Delete the status.
    $query = \Drupal::entityQuery('maestro_process_status')
      ->condition('process_id', $processID)
      ->accessCheck(FALSE);
    $entityIDs = $query->execute();
    foreach ($entityIDs as $entityID) {
      $record = \Drupal::entityTypeManager()->getStorage('maestro_process_status')->load($entityID);
      if ($record) {
        $record->delete();
      }
    }

    // finally, delete the process.
    $processRecord = MaestroEngine::getProcessEntryById($processID);
    if ($processRecord) {
      $processRecord->delete();
    }
  }
  
  /**
   * getQueueItemTaskData  
   * Provide a Maestro Queue ID and this method will return to you the LIVE queue item's task data as an array.
   * The task data is seeded by the task's configuration settings upon creation.
   * @param  int $queueID  
   *   The integer Queue ID you wish to load  
   * @param  string|null $configuration_key  
   *   Optional.  Some tasks save their data within a sub-array. For example, the Maestro Webform task does not use a 
   *   sub-array, however, the Set Process Variable task does use an "spv" sub-array.  
   *   If you omit the configuration key, the return array will be zero-keyed. For example:  
   *   <pre>
   * &nbsp;Array [  
   * &nbsp;&nbsp;&nbsp;&nbsp;[0] => [  
   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;....  
   * &nbsp;&nbsp;&nbsp;&nbsp;]  
   * &nbsp;]  
   * </pre>
   * @return array  
   *   Returns an empty array when the queue item is not able to be loaded.  Otherwise an array of the task data.
   */
  public static function getQueueItemTaskData($queueID, $configuration_key = NULL) {
    $maestro_queue = MaestroEngine::getQueueEntryById($queueID);
    $queue_task_data = [];
    if($maestro_queue)   {
      if($configuration_key) {
        $queue_task_data = $maestro_queue->get('task_data')->{$configuration_key} ?? []; // This is LIVE data vs. config data  
      }
      else {
        $queue_task_data = $maestro_queue->get('task_data')->getValue() ?? []; // This is LIVE data vs. config data
      }
    }  
    return $queue_task_data;
  }
  
  /**
   * setQueueItemTaskData  
   * Sets the $taskDataKey key to the $value provided.  If your task data lives under a sub-array key, 
   * specify the key with $configuration_key.
   *
   * @param  int $queueID  
   *   The Maestro Queue ID  
   * @param  string $taskDataKey  
   *   The key you are trying to set to $value  
   * @param  mixed $value  
   *   The value you are setting to the $taskDataKey  
   * @param  string $configuration_key  
   *   Optional.  Some tasks save their data within a sub-array. For example, the Maestro Webform task does not use a 
   *   sub-array, however, the Set Process Variable task does use an "spv" sub-array.  
   * @return void
   */
  public static function setQueueItemTaskData($queueID, $taskDataKey, $value = NULL, $configuration_key = NULL) {
    $maestro_queue = MaestroEngine::getQueueEntryById($queueID);
    $queue_task_data = MaestroEngine::getQueueItemTaskData($queueID, $configuration_key);
    if($configuration_key !== NULL) { // Could be a 0!
      $queue_task_data[0][$configuration_key][$taskDataKey] = $value;
    }
    else {
      $queue_task_data[0][$taskDataKey] = $value;
    }
    $maestro_queue->set('task_data', $queue_task_data);
    $maestro_queue->save();
  }
  
  /**
   * setSuspensionFlag  
   * Provide a Maestro Queue ID and the suspension flag and this will set the 
   * task's queue entry inside of the task_data column's value to the 
   * specified value.
   *
   * @param  int $queueID  
   *   The Maestro Queue ID  
   * @param  mixed $suspension_flag  
   *   The suspension flag value. The built in values are  
   *   MAESTRO_UNSUSPENDED  
   *   MAESTRO_SUSPEND  
   *   MAESTRO_ALLOW_SUSPENDED_COMPLETION 
   * @return void
   */
  public static function setSuspensionFlag($queueID, $suspension_flag = MAESTRO_SUSPEND) {
    MaestroEngine::setQueueItemTaskData($queueID, 'maestro_engine_suspend', $suspension_flag);
    if($suspension_flag == MAESTRO_SUSPEND) {
      MaestroEngine::setTaskStatus($queueID, TASK_STATUS_HOLD);
    }
    else {
      MaestroEngine::setTaskStatus($queueID, TASK_STATUS_ACTIVE);
    }
    
  }

  /**
   * getSuspensionFlag  
   * Provide a Maestro Queue ID and this method will return the suspension flag
   * value.
   *
   * @param  int $queueID  
   *   The Maestro Queue ID.  
   * @return int  
   *   Returns one of the Maestro suspension flag values:  
   *   MAESTRO_UNSUSPENDED  
   *   MAESTRO_SUSPEND  
   *   MAESTRO_ALLOW_SUSPENDED_COMPLETION
   */
  public static function getSuspensionFlag($queueID) {
    $task_data = MaestroEngine::getQueueItemTaskData($queueID);
    $suspension_flag = $task_data[0]['maestro_engine_suspend'] ?? 0;
    return $suspension_flag;
  }

  /*
   *************************************
   * End of static api functionality.
   *************************************
   */


  /*
   ***************************************
   * API for dealing with the queue.
   ***************************************
   */

  /**
   * NewProcess
   * Creates a new process in the Maestro content entities based on the template name which is mandatory.
   * This method will only create a new process if the template has the validated property set.
   *
   * @param string $templateName
   *   Machine name of the template you wish to kick off.
   * @param string $startTask
   *   The offset starting task you wish to use as your first task.  Default is 'start'.
   *
   * @return int|bool
   *   Returns the process ID if the engine has started the process.  FALSE if there was an issue.
   *   Please use === to ensure that you are testing for FALSE and not 0 as there may be other issues with the save.
   */
  public function newProcess($templateName, $startTask = 'start') {
    // Pessimistic return.
    $process_id = FALSE;
    $template = $this->getTemplate($templateName);
    if (!isset($template->validated) || $template->validated == FALSE) {
      if ($this->debug) {
        \Drupal::messenger()->addError(t('This template has not been validated.  You must validate before launching.'));
      }
      return FALSE;
    }
    if ($template !== NULL) {
      $values = [
        'process_name' => $template->label,
        'template_id' => $template->id,
        'complete' => 0,
        'initiator_uid' => \Drupal::currentUser()->id(),
      ];
      $new_process = \Drupal::entityTypeManager()->getStorage('maestro_process')->create($values);
      $new_process->save();
      if ($new_process->id()) {
        // The process has been kicked off and we're ready to add the particulars to the queue and variables.
        $process_id = $new_process->id();
        // Now to add variables.
        $variables = $template->variables;
        foreach ($variables as $variable) {
          $values = [
            'process_id' => $process_id,
            'variable_name' => $variable['variable_id'],
            'variable_value' => $variable['variable_value'],
          ];
          // Handle any mandatory variable presetting here.
          switch ($variable['variable_id']) {
            case 'initiator':
              $values['variable_value'] = \Drupal::currentUser()->getAccountName();
              break;

            // Pull from the workflow template and populate the variable.
            case 'workflow_timeline_stage_count':
              $values['variable_value'] = $template->default_workflow_timeline_stage_count;
              break;

            // Starting of any process is step 0.
            case 'workflow_current_stage':
              $values['variable_value'] = 0;
              break;

            // Blank out the current stage/status message.
            case 'workflow_current_stage_message':
              $values['variable_value'] = '';
              break;
          }

          $new_var = \Drupal::entityTypeManager()->getStorage('maestro_process_variables')->create($values);
          $new_var->save();
          if (!$new_var->id()) {
            // Throw a maestro exception
            // completion should technically end here for this initiation.
            throw new MaestroSaveEntityException('maestro_process_variable', $variable['variable_id'] . ' failed saving during new process creation.');
          }
        }
        // Now to add the process status and stage information
        // we do this by looping through the tasks and creating an array of values for which we then set
        // the maestro_process_status entity.
        $status_message_array = [];
        foreach ($template->tasks as $task) {
          // Relates to the checkbox on the task editor.
          if (isset($task['participate_in_workflow_status_stage']) && $task['participate_in_workflow_status_stage'] == 1) {
            if (isset($task['workflow_status_stage_number'])) {
              $status_message_array[$task['workflow_status_stage_number']] = $task['workflow_status_stage_message'];
            }
          }
        }
        if (count($status_message_array)) {
          // We have status messages to set.
          foreach ($status_message_array as $stage_number => $stage_message) {
            $values = [
              'process_id' => $process_id,
              'stage_number' => $stage_number,
              'stage_message' => $stage_message,
            ];
            $new_stage = \Drupal::entityTypeManager()->getStorage('maestro_process_status')->create($values);
            $new_stage->save();
            if (!$new_stage->id()) {
              // Throw a maestro exception
              // completion should technically end here for this initiation.
              throw new MaestroSaveEntityException('maestro_process_status', 'Stage ' . $variable['stage_number'] . ' status message failed saving during new process creation.');
            }
          }
        }

        // Now to add the initiating task.
        $start_task = $template->tasks[$startTask];
        if (is_array($start_task)) {
          $values = [
            'process_id' => $process_id,
            'task_class_name' => $start_task['tasktype'],
            'task_id' => $start_task['id'],
            'task_label' => $start_task['label'],
            'engine_version' => 2,
            'is_interactive' => $start_task['assignto'] == 'engine' ? 0 : 1,
            'show_in_detail' => isset($start_task['showindetail']) ? $start_task['showindetail'] : 0,
            'handler' => isset($start_task['handler']) ? $start_task['handler'] : '',
            'task_data' => isset($start_task['data']) ? $start_task['data'] : '',
            'status' => 0,
            'run_once' => isset($start_task['runonce']) ? $start_task['runonce'] : 0,
            'uid' => \Drupal::currentUser()->id(),
            'archived' => 0,
            'started_date' => time(),
            'token' => Crypt::randomBytesBase64(),
          ];

          $queue = \Drupal::entityTypeManager()->getStorage('maestro_queue')->create($values);
          $queue->save();
          if ($queue->id()) {
            // Successful queue insertion
            // do any assignments here.
            $this->productionAssignments($templateName, $startTask, $queue->id());
          }
          else {
            // Throw a maestro exception.
            throw new MaestroSaveEntityException('maestro_queue', $start_task['tasktype'] . ' failed saving new task during new process creation.');
          }
        }
        else {
          // We have an issue here.  Throw some sort of exception that we can catch.
          // for now, ignore this case.
          throw new MaestroGeneralException('Start task for template ' . $template->id . ' may be corrupt.');
        }
      }
    }
    return $process_id;
  }

  /**
   * CleanQueue method
   * This is the core method used by the orchestrator to move the process forward and to determine assignments and next steps.
   */
  public function cleanQueue() {
    $config = \Drupal::config('maestro.settings');

    // We first check to see if there are any tasks that need processing
    // we do this by looking at the queue flags to determine if the
    // task is not archived, not completed and hasn't run once.
    $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
    $query->condition('archived', '0')
      ->accessCheck(FALSE)
      ->condition('status', '0')
      ->condition('is_interactive', '0')
      ->condition('run_once', '0')
      ->condition('process_id.entity.complete', '0');
    $entity_ids = $query->execute();
    // This gives us a list of entity IDs that we can use to determine the state of the process and what, if anything, we
    // have to do with this task.
    // now we need interactive tasks that have a completion status.
    $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
    $query->condition('archived', '0')
      ->accessCheck(FALSE)
      ->condition('is_interactive', '1')
    // This allows the interactive tasks to set their status.
      ->condition('status', '0', '<>')
      ->condition('run_once', '1')
      ->condition('process_id.entity.complete', '0');

    $entity_ids += $query->execute();
    ksort($entity_ids);
    foreach ($entity_ids as $queueID) {
      // queueID is the numeric ID of the entity ID.  Load it, but first clear any cache if dev mode is on.
      if ($this->developmentMode) {
        $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->resetCache([$queueID]);
      }
      $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
      $processID = $queueRecord->process_id->getString();
      if ($this->developmentMode) {
        $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->resetCache([$processID]);
      }
      $processRecord = \Drupal::entityTypeManager()->getStorage('maestro_process')->load($processID);
      $taskClassName = $queueRecord->task_class_name->getString();
      $taskID = $queueRecord->task_id->getString();
      $templateMachineName = $processRecord->template_id->getString();
      $suspension_flag = MaestroEngine::getSuspensionFlag($queueID);
      // If the process is not complete AND the task is not suspended
      if ($processRecord->complete->getString() == '0' && $suspension_flag != MAESTRO_SUSPEND) {
        // Execute it!
        $task = $this->getPluginTask($taskClassName, $processID, $queueID);
        // Its a task and not an interactive task.
        if ($task && !$task->isInteractive()) {
          $result = $task->execute();
          if ($result === TRUE) {
            // We now set the task's status and create the next task instance!
            $queueRecord->set('status', $task->getExecutionStatus());
            $queueRecord->set('completed', time());
            $queueRecord->save();
            $this->nextStep($templateMachineName, $taskID, $processID, $task->getCompletionStatus());
            $this->archiveTask($queueID);
          }
        }
        else {
          // If it IS an interactive task.
          if ($task && $task->isInteractive()) {
            $this->nextStep($templateMachineName, $taskID, $processID, $task->getCompletionStatus());
            $this->archiveTask($queueID);
          }
          else {
            // This plugin task definition doesn't exist and its not interactive, however, throwing an exception will lock up the engine.
            // we will throw the exception here knowing that this exception will lock the engine at this point.
            // This process is doomed for failure anyway, too bad it's causing the engine to stall...
            // suggestion here is to create a new status of "error", apply it to the task, which the engine will skip over and we can report on easily with views.
            throw new MaestroGeneralException('Task definition doesn\'t exist. TaskID:' . $taskID . ' in ProcessID:' . $processID . ' is not flagged as interactive or non-interactive.');
          }
        }
      } //end if process is not completed (0)
    } //end foreach through open queue items

    // Retaining this section for now to determine if we should be closing any open tasks that have a process that is complete.
    // this has no effect on the engine either way.
    /* //Now we check for queue items that need to be archived
    //These are tasks that have a status of 1 (complete) and are not yet archived
    $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
    $query->condition('archived', '0')
    ->condition('status', '0', '<>')
    ->condition('process_id.entity.complete', '0');
    $entity_ids = $query->execute();
    foreach($entity_ids as $queueID) {
    //TODO: pre-archive hook?
    $this->archiveTask($queueID);
    //TODO: post archive hook?
    } */

    // TODO: pull notifications out like this to its own cron hook?
    if ($config->get('maestro_send_notifications')) {
      $currentTime = time();
      // So now only for interactive tasks that are not complete and have aged beyond their reminder intervals.
      $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
      $query->condition('archived', '0')
        ->accessCheck(FALSE)
        ->condition('is_interactive', '1')
        ->condition('status', '0')
        ->condition('run_once', '1')
        ->condition('next_reminder_time', $currentTime, '<')
        ->condition('next_reminder_time', '0', '<>')
        ->condition('reminder_interval', '0', '>');
      $entity_ids = $query->execute();
      // Now for each of these entity_ids, we send out a reminder.
      foreach ($entity_ids as $queueID) {
        // We know that because we're in this loop, these interactive tasks require reminders.
        if ($this->developmentMode) {
          $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->resetCache([$queueID]);
        }
        $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
        $taskMachineName = $queueRecord->task_id->getString();
        $templateMachineName = MaestroEngine::getTemplateIdFromProcessId($queueRecord->process_id->getString());
        // Just days * seconds to get an offset.
        $reminderInterval = intval($queueRecord->reminder_interval->getString()) * 86400;
        // we're in here because we need a reminder.  So do it.
        $this->doProductionAssignmentNotifications($templateMachineName, $taskMachineName, $queueID, 'reminder');
        $queueRecord->set('next_reminder_time', $currentTime + $reminderInterval);
        $queueRecord->set('num_reminders_sent', intval($queueRecord->num_reminders_sent->getString()) + 1);
        $queueRecord->save();
      }

      // Now for escalations.
      $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
      $query->condition('archived', '0')
        ->accessCheck(FALSE)
        ->condition('is_interactive', '1')
        ->condition('status', '0')
        ->condition('run_once', '1')
        ->condition('escalation_interval', 0, '>');
      $entity_ids = $query->execute();
      foreach ($entity_ids as $queueID) {
        // So for only those queue records that have an escalation interval
        // has this task aged beyond the escalation interval number of days since it was created?  if so, notify.
        if ($this->developmentMode) {
          $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->resetCache([$queueID]);
        }
        $queueRecord = \Drupal::entityTypeManager()->getStorage('maestro_queue')->load($queueID);
        $taskMachineName = $queueRecord->task_id->getString();
        $templateMachineName = MaestroEngine::getTemplateIdFromProcessId($queueRecord->process_id->getString());
        $createdTime = $queueRecord->created->getString();
        $numberOfEscalationsSent = intval($queueRecord->num_escalations_sent->getString());
        // First time through, numberOfEscalations is 0... second time it's 1 etc.
        // that means that our interval needs to be numberOfEscalations +1 * the offset of the escalation in days.
        $escalationInterval = (1 + $numberOfEscalationsSent) * (intval($queueRecord->escalation_interval->getString()) * 86400);
        if ($currentTime > ($createdTime + $escalationInterval)) {
          // We need to send out an escalation.
          $this->doProductionAssignmentNotifications($templateMachineName, $taskMachineName, $queueID, 'escalation');
          $queueRecord->set('last_escalation_time', $currentTime);
          $queueRecord->set('num_escalations_sent', intval($queueRecord->num_escalations_sent->getString()) + 1);
          $queueRecord->save();
        }
      }
    }

  } //end cleanQueue

  /*
   ***************************************
   * Protected methods used by the engine.
   ***************************************
   */

  /**
   * NextStep
   * Engine method that determines which is the next step based on the current step
   * and does all assignments as necessary.
   *
   * @param string $template
   *   The Maestro template.
   * @param string $templateTaskID
   *   The machine name of the task.
   * @param int $processID
   *   The Maestro Process ID.
   * @param int $completionStatus
   *   The completion status being set.  Definitions found in maestro.module.
   */
  protected function nextStep($template, $templateTaskID, $processID, $completionStatus) {
    $templateTask = $this->getTemplateTaskByID($template, $templateTaskID);
    $regenerationFlag = FALSE;
    // Nextstep or nextfalsestep is a comma separated string of next task machine names to point to.
    $nextSteps = $templateTask['nextstep'];
    // Completion status tells us to point to the false branch.
    if ($completionStatus == MAESTRO_TASK_COMPLETION_USE_FALSE_BRANCH) {
      $nextSteps = $templateTask['nextfalsestep'];
    }
    // TODO:  In the event we have a feature request for extra completion status codes, at this point we've established the core Maestro status codes and,
    // have altered the next steps to respect the IF task scenario.
    // Is there really any other scenario?  We've never come across this issue.  But this would be the spot in-code to allow for a
    // module invocation to alter the next steps based on completion status.
    if ($nextSteps != '') {
      $taskArray = explode(',', $nextSteps);
      foreach ($taskArray as $taskID) {
        // Determine if this task is already present in this instance of the queue/process combo
        // but first, determine if they're trying to recreate a task that has already been completed
        // this is our chance to do an auto-regeneration
        // we also filter for the tasks not being an OR or AND task.
        $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
        // Race condition?  what if its complete and not archived, yet a loopback happens?  Leave for now.
        $query->condition('archived', TASK_ARCHIVE_REGEN, '<>')
          ->accessCheck(FALSE)
          ->condition('status', TASK_STATUS_ACTIVE, '<>')
          ->condition('process_id', $processID)
          ->condition('task_id', $taskID)
        // Task is not an OR.
          ->condition('task_class_name', 'MaestroOr', '<>')
        // Task is not an AND.
          ->condition('task_class_name', 'MaestroAnd', '<>');
        $entity_ids = $query->execute();
        if (count($entity_ids) == 0) {
          // No regeneration!  this is a straightforward engine carry-on condition
          // in Drupal 7's engine, we had flags to check to see if a task REEEEEEALY wanted to be regenerated.
          // no more.  After 10 years of engine development, we've found that regeneration of all in-prod tasks is the way to go
          // look at the ELSE clause to see the regen.
          $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
          // We don't need to recreate if this thing is already in the queue.
          $query->condition('archived', '0')
            ->accessCheck(FALSE)
            ->condition('status', TASK_STATUS_ACTIVE)
            ->condition('process_id', $processID)
            ->condition('task_id', $taskID);
          $entity_ids = $query->execute();
          // Means we haven't already created it in this process. avoids mimicking the regen issue.
          if (count($entity_ids) == 0) {
            // Detect if this is an AND task we're about to create.
            // If so, see if it has already been completed in this process instance
            // and is not a regen.
            $nextTask = $this->getTemplateTaskByID($template, $taskID);
            if($nextTask['tasktype'] == 'MaestroAnd') {
              $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
              $query->condition('archived', TASK_ARCHIVE_REGEN, '<>')
                ->accessCheck(FALSE)
                ->condition('status', TASK_STATUS_SUCCESS)
                ->condition('process_id', $processID)
                ->condition('task_id', $taskID);
              $entity_ids = $query->execute();
              if (count($entity_ids) == 0) {
                $queueID = $this->createProductionTask($taskID, $template, $processID);
              }
            }
            else {
              $queueID = $this->createProductionTask($taskID, $template, $processID);
            }

          }
        }
        // REGENERATION.
        else {
          // It is in this area where we are doing a complete loopback over our existing template
          // after years of development experience and creating many business logic templates, we've found that
          // the overwhelming majority (like 99%) of all templates really do a regeneration of all
          // in-production tasks and that people really do want to do a regeneration.
          // Thus the regen flags have been omitted.  Now we just handle everything with status flags and keep the same
          // process ID.
          // The biggest issue are the AND tasks.  We need to know which tasks the AND has pointing to it and keep those
          // tasks hanging around in the queue in either a completed and archived state or in their fully open, executable state.
          // so we have to first find all AND tasks, and then determine who points to them and leave their archive condition alone.
          $noRegenStatusArray = [];
          // So first, search for open AND tasks:
          $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
          $query
            ->condition('archived', TASK_ARCHIVE_REGEN, '<>')
            ->accessCheck(FALSE)
            ->condition('status', '0')
            ->condition('process_id', $processID)
          // Task is an AND.
            ->condition('task_class_name', 'MaestroAnd');
          // Going to use these IDs to determine who points to them.
          $andIDs = $query->execute();
          if (is_array($andIDs)) {
            $noRegenStatusArray += $andIDs;
          }
          foreach ($andIDs as $entityID) {
            // Load the entity from the queue.
            $queueRecord = MaestroEngine::getQueueEntryById($entityID);
            $pointers = MaestroEngine::getTaskPointersFromTemplate(MaestroEngine::getTemplateIdFromProcessId($processID), $queueRecord->task_id->getString());
            // Now we query the queue to add the pointers to the noRegenStatusArray.
            $query = \Drupal::entityQuery('maestro_queue')
              ->accessCheck(FALSE);
            $andMainConditions = $query->andConditionGroup()
              ->condition('process_id', $processID);
            $orConditionGroup = $query->orConditionGroup();
            foreach ($pointers as $pointer) {
              $orConditionGroup->condition('task_id', $pointer);
            }
            if(count($pointers)) {
              $andMainConditions->condition($orConditionGroup);
            }
            $query->condition($andMainConditions);
            $pointerIDs = $query->execute();
            if (is_array($pointerIDs)) {
              // Add the entity IDs to the tasks that point to the AND as those that shouldn't be flagged.
              $noRegenStatusArray += $pointerIDs;
            }
          }
          // now, we have a list of noRegenStatusArray which are entity IDs in the maestro_queue for which we do NOT change the archive flag for.
          $query = \Drupal::entityTypeManager()->getStorage('maestro_queue')->getQuery();
          $query->condition('status', '0', '<>')
            ->accessCheck(FALSE)
          // All completed tasks that haven't been regen'd.
            ->condition('process_id', $processID);
          $regenIDs = $query->execute();
          foreach ($regenIDs as $entityID) {
            // Set this queue record to regenerated IF it doesn't exist in the noRegenStatusArray.
            if (array_search($entityID, $noRegenStatusArray) === FALSE) {
              $queueRecord = MaestroEngine::getQueueEntryById($entityID);
              $queueRecord->set('archived', TASK_ARCHIVE_REGEN);
              $queueRecord->save();
            }
          }
          // And now we create the task being looped back over to:
          $queueID = $this->createProductionTask($taskID, $template, $processID);
        }
      }//end foreach next task
    }
    else {
      // This is the condition where there isn't a next step listed in the task
      // this doesn't necessarily suggest an end of process though, as that's what
      // the end task is meant for.  However, we have a situation here where
      // the task doesn't specify a next task.  Perhaps this is legitimately the end
      // of a task chain while other parallel tasks continue to execute.
      // We will consider this a NOOP condition and do nothing.
    }

  }//end nextStep

  /**
   * Using the queueID, the task's machine name and the template machine name, we assign the task
   * using the appropriate method defined in the template.
   *
   * @param string $templateMachineName
   *   Machine name of the template.
   * @param string $taskID
   *   Machine name of the task.
   * @param int $queueID
   *   The ID of the queue entity this task belongs to.
   */
  protected function productionAssignments($templateMachineName, $taskID, $queueID) {
    $config = \Drupal::config('maestro.settings');
    $task = $this->getTemplateTaskByID($templateMachineName, $taskID);
    $executableTask = MaestroEngine::getPluginTask($task['tasktype']);
    $assigned = '';
    // For tasks that have not set the assignment as they're engine tasks.
    if (array_key_exists('assigned', $task)) {
      $assigned = $task['assigned'];
    }

    // If the assignment is blank ir set to engine, and it's not an interactive task this is an engine assignment.
    if (($assigned == '' || $assigned == 'engine') && !$executableTask->isInteractive()) {
      return;
    }
    if ($assigned == '' && $executableTask->isInteractive()) {
      // hmm.  this is a condition where the template says there's nobody assigned, yet the task says it's an interactive.
      // TODO:  Throw an error here?  Do an invoke to modules to see if they know why?
      // for now, we return and let it be assigned to the engine.
      return;
    }

    // The format of the assignment is as follows
    // towhat:by:who   example:  user:fixed:admin,role:variable:var_name.
    $assignments = explode(',', $assigned);
    foreach ($assignments as $assignment) {
      $thisAssignment = explode(':', $assignment);
      // [0] is user, role etc.  [1] is how such as fixed or by variable.  [2] is to who or which var
      // Assigned by fixed name.
      if ($thisAssignment[1] == 'fixed') {
        $values = [
          'queue_id' => $queueID,
          'assign_type' => $thisAssignment[0],
          'by_variable' => 0,
          'assign_id' => $thisAssignment[2],
          'process_variable' => 0,
          'assign_back_id' => 0,
          'task_completed' => 0,
        ];
        $prodAssignments = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->create($values);
        $prodAssignments->save();
      }
      // Assigned by variable.
      elseif ($thisAssignment[1] == 'variable') {
        $var = MaestroEngine::getProcessVariable($thisAssignment[2], MaestroEngine::getProcessIdFromQueueId($queueID));
        $varID = MaestroEngine::getProcessVariableID($thisAssignment[2], MaestroEngine::getProcessIdFromQueueId($queueID));
        // Now to use the information supplied to us from [0] to determine is this a user or role and then also let other modules do their own thing here.
        $assignmentsByVar = explode(',', $var);
        foreach ($assignmentsByVar as $assignTo) {
          // What if the variable is blank?  This is an assignment to a variable that may have gone wrong and this could orphan a task.
          // Let's create the task, assign it to a very specific non-user so that it is visible in the assignments and views.
          if ($assignTo == '') {
            $assignTo = 'unable-to-assign'; // We force it to a non-user. If a user is actually named "unable-to-assign", well...
          }
          $values = [
            'queue_id' => $queueID,
            'assign_type' => $thisAssignment[0],
            'by_variable' => 1,
            'assign_id' => $assignTo,
            'process_variable' => $varID,
            'assign_back_id' => 0,
            'task_completed' => 0,
          ];
          $prodAssignments = \Drupal::entityTypeManager()->getStorage('maestro_production_assignments')->create($values);
          $prodAssignments->save();
          
        }

      }
    }

    // And now we do a module invoke for any add-on modules to do their own assignments or tweak the assignments.
    \Drupal::moduleHandler()->invokeAll('maestro_post_production_assignments',
        [$templateMachineName, $taskID, $queueID]);

  }

  /**
   * Creates a task in the Maestro Queue table.
   *
   * @param string $taskMachineName
   *   The machine name of the task.
   * @param string $templateMachineName
   *   The machine name of the template.
   * @param int $processID
   *   The Maestro Process ID.
   *
   * @return int|bool
   *   Returns FALSE on no queue entry.  Returns the QueueID upon success.
   */
  protected function createProductionTask($taskMachineName, $templateMachineName, $processID) {
    $config = \Drupal::config('maestro.settings');
    $queueID = FALSE;
    $nextTask = $this->getTemplateTaskByID($templateMachineName, $taskMachineName);
    $executableTask = MaestroEngine::getPluginTask($nextTask['tasktype']);

    $currentTime = time();
    $nextReminderTime = 0;
    $reminderInterval = 0;
    $escalationInterval = 0;
    if (is_array($nextTask) && array_key_exists('notifications', $nextTask)) {
      $reminderInterval = $nextTask['notifications']['reminder_after'];
      if (intval($reminderInterval) > 0) {
        $nextReminderTime = $currentTime + (intval($reminderInterval) * 86400);
      }
      $escalationInterval = $nextTask['notifications']['escalation_after'];
    }

    $values = [
      'process_id' => $processID,
      'task_class_name' => $nextTask['tasktype'],
      'task_id' => $nextTask['id'],
      'task_label' => $nextTask['label'],
      'engine_version' => 2,
      'is_interactive' => $executableTask->isInteractive() ? 1 : 0,
      'show_in_detail' => isset($nextTask['showindetail']) ? $nextTask['showindetail'] : 0,
      'handler' => isset($nextTask['handler']) ? $nextTask['handler'] : '',
      'task_data' => isset($nextTask['data']) ? $nextTask['data'] : '',
      'status' => 0,
      'run_once' => $executableTask->isInteractive() ? 1 : 0,
    // This should probably be 0 to signify the engine.
      'uid' => 0,
      'archived' => 0,
      'started_date' => $currentTime,
      'num_reminders_sent' => 0,
      'num_escalations_sent' => 0,
      'next_reminder_time' => $nextReminderTime,
      'reminder_interval' => $reminderInterval,
      'escalation_interval' => $escalationInterval,
      'token' => Crypt::randomBytesBase64(),
    ];
    $queue = \Drupal::entityTypeManager()->getStorage('maestro_queue')->create($values);
    $queue->save();
    // Now to do assignments if the queue ID has been set.
    if ($queue->id()) {
      // Perform maestro assignments.
      $this->productionAssignments($templateMachineName, $taskMachineName, $queue->id());
      $queueID = $queue->id();
      if ($config->get('maestro_send_notifications')) {
        $this->doProductionAssignmentNotifications($templateMachineName, $taskMachineName, $queue->id(), 'assignment');
      }
      // Now lets set the workflow status process variables if the task requires us to do so.
      // Relates to the checkbox on the task editor.
      if (isset($nextTask['participate_in_workflow_status_stage']) && $nextTask['participate_in_workflow_status_stage'] == 1) {
        if (isset($nextTask['workflow_status_stage_number'])) {
          $this->setProcessVariable('workflow_current_stage', $nextTask['workflow_status_stage_number'], $processID);
        }
        if (isset($nextTask['workflow_status_stage_message'])) {
          $this->setProcessVariable('workflow_current_stage_message', $nextTask['workflow_status_stage_message'], $processID);
        }
      }
    }
    else {
      // TODO: throw maestro exception here.
    }
    return $queueID;
  }

  /**
   * Internal method to do production assignment notifications.
   */
  protected function doProductionAssignmentNotifications($templateMachineName, $taskMachineName, $queueID, $notificationType = 'assignment') {
    $config = \Drupal::config('maestro.settings');
    $queueToken = MaestroEngine::getTokenFromQueueId($queueID);
    $templateTask = $this->getTemplateTaskByID($templateMachineName, $taskMachineName);
    $notificationList = [];
    if (!empty($templateTask['notifications']['notification_assignments'])) {
      $notifications = explode(',', $templateTask['notifications']['notification_assignments']);
      foreach ($notifications as $notification) {
        // We will assume that WE are the ones doing the notification.  Otherwise we're offloading to a different module to do so.
        $doNotification = TRUE;
        // [0] is to what type of entity, [1] is fixed or variable, [2] is entity or variable, [3] is type
        $thisNotification = explode(':', $notification);
        // Works for any assignment type passed in.
        if ($thisNotification[3] == $notificationType) {
          $entity = '';
          if ($thisNotification[1] == 'fixed') {
            $entity = $thisNotification[2];
          }
          elseif ($thisNotification[1] == 'variable') {
            // Variable is in [2].  Need to get its value.
            $processID = MaestroEngine::getProcessIdFromQueueId($queueID);
            $variableValue = MaestroEngine::getProcessVariable($thisNotification[2], $processID);
            // Assumption here is that these values are actual names of users or roles or whatever other entity.
            $entity = $variableValue;
          }
          // Oh oh.  not fixed or by variable.  Don't do this notification.
          else {
            $doNotification = FALSE;
          }

          if ($thisNotification[0] == 'user' && $doNotification) {
            // Load the user(s) by name and get their email addr.
            $users = explode(',', $entity);
            foreach ($users as $accountName) {
              if ($accountName != '') {
                $account = user_load_by_name($accountName);
                if ($account) {
                  $notificationList[$account->get('mail')->getString()] = $account->get('mail')->getString();
                }
                // There is no account by that name.  Log this as an exception.
                else {
                  throw new MaestroGeneralException('Unknown account name identified when attempting a notification.');
                }
              }
            }
          }
          elseif ($thisNotification[0] == 'role' && $doNotification) {
            // Have to parse out WHO is in the role and then get their email addr.
            $roles = explode(',', $entity);
            foreach ($roles as $roleName) {
              if ($roleName != '') {
                $ids = \Drupal::entityQuery('user')
                  ->accessCheck(FALSE)
                  ->condition('status', 1)
                  ->condition('roles', $roleName)
                  ->execute();
                $users = User::loadMultiple($ids);
                foreach ($users as $account) {
                  $notificationList[$account->get('mail')->getString()] = $account->get('mail')->getString();
                }
              }
            }
          }
          else {
            // This is not a Maestro-managed assignment type of role or user whether that be by variable or fixed.
            // Manage this yourself via this hook.
            \Drupal::moduleHandler()->invokeAll('maestro_production_' . $notificationType . '_notification',
                [$queueID, $thisNotification, &$notificationList]);
          }
        }
      } //end foreach over each assignment
      // ok, now we can check to see if we have any assignments.
      if (count($notificationList) > 0) {
        $notificationMessage = '';
        if (array_key_exists('notification_' . $notificationType, $templateTask['notifications']) && $templateTask['notifications']['notification_' . $notificationType] != '') {
          $notificationMessage = $templateTask['notifications']['notification_' . $notificationType];
          // Now do a token replacement
          $tokenService = \Drupal::token();
          $notificationMessage = $tokenService->replace($notificationMessage, ['maestro' => ['task' => $templateTask, 'queueID' => $queueID]]);
          if ($notificationType == 'assignment') {
            $subject = array_key_exists('notification_assignment_subject', $templateTask['notifications']) ? $tokenService->replace($templateTask['notifications']['notification_assignment_subject'], ['maestro' => ['task' => $templateTask, 'queueID' => $queueID, 'queueToken' => $queueToken]]) : 'You have a new task assignment';
          }
          elseif ($notificationType == 'reminder') {
            $subject = array_key_exists('notification_reminder_subject', $templateTask['notifications']) ? $tokenService->replace($templateTask['notifications']['notification_reminder_subject'], ['maestro' => ['task' => $templateTask, 'queueID' => $queueID, 'queueToken' => $queueToken]]) : 'You have a new task assignment';
          }
          elseif ($notificationType == 'escalation') {
            $subject = array_key_exists('notification_escalation_subject', $templateTask['notifications']) ? $tokenService->replace($templateTask['notifications']['notification_escalation_subject'], ['maestro' => ['task' => $templateTask, 'queueID' => $queueID, 'queueToken' => $queueToken]]) : 'You have a new task assignment';
          }
        }
        // Default built in message.
        else {
          // Strip off a trailing slash as we're going to add it ourselves.
          $redirectionLocation = rtrim($config->get('maestro_redirect_location'), '/');
          if ($redirectionLocation == '') {
            $redirectionLocation = '/taskconsole';
          }
          $queueRecord = MaestroEngine::getQueueEntryById($queueID);
          if ($notificationType == 'assignment') {
            $notificationMessage = t('A new task titled:') . ' ' . $queueRecord->task_label->getString() . ' ' . t('has been assigned to you.');
            $notificationMessage .= t('Click here to go to see your tasks:') . ' ' . '<a href="' . Url::fromUserInput($redirectionLocation . '/' . $queueToken, ['absolute' => TRUE])->toString() . '">Task Console</a>';
          }
          elseif ($notificationType == 'reminder') {
            $notificationMessage = t('A reminder you have open tasks.  Please review the task:') . ' ' . $queueRecord->task_label->getString();
            $notificationMessage .= t('Click here to go to your tasks:') . ' <a href="' . Url::fromUserInput($redirectionLocation . '/' . $queueToken, ['absolute' => TRUE])->toString() . '">Task Console</a>';
          }
          elseif ($notificationType == 'escalation') {
            $notificationMessage = t('An escalation for an overdue task has been generated for the task:') . ' ' . $queueRecord->task_label->getString();
            // The escalation will have to have more information associated with it.. who is assigned, when it was assigned etc etc.
            // TODO:  need the list of assigned users/roles/whatever.
            $notificationMessage .= t('Assigned To:') . ' ';
            $notificationMessage .= t('Queue ID:') . ' ' . $queueID;
            $notificationMessage .= t('Queue Token:') . ' ' . $queueToken;
            $notificationMessage .= t('Process IDr:') . ' ' . $queueRecord->process_id->getString();
          }
        }
        $mailManager = \Drupal::service('plugin.manager.mail');
        $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
        $params = [];
        $params['queueID'] = $queueID;
        $tokenService = \Drupal::token();
        $params['subject'] = isset($subject) ? $subject : t('You have a new task assignment');
        $params['message'] = $notificationMessage;
        foreach ($notificationList as $email) {
          $result = $mailManager->mail('maestro', $notificationType . '_notification', $email, $langcode, $params, NULL, TRUE);
        }
      }
      else {
        /*
         * This is the scenario where we have no assignees/actors listed.
         * NOTE: this will NEVER fire if you have an explicit actor or role with known Drupal users within it, assigned to this task.
         *
         * There is a use case where a Maestro task is assigned to a known entity, however, they are not a part of the Drupal users.
         * For example: a webform could be presented to a person who does not have a Drupal account and still requires
         * to provide information to complete a workflow.  An outgoing message can be sent to known non-Drupal actors of a specific task or task type
         * using the Maestro site-wide Token in the URL.  It is up to the developers to write a completion routine and check the
         * validity of the non-Drupal actor.
         *
         * We will check to see if Maestro is configured to use the outgoing Maestro Zero-User option, thereby allowing
         * Developers to create a custom message.
         */

         if($config->get('maestro_token_zero_user') == 1) { //This can only be set if the token key has been set.
           //Developers can fetch the Queue Token via the Maestro API in their hook.
           \Drupal::moduleHandler()->invokeAll('maestro_zero_user_notification', [$templateMachineName, $taskMachineName, $queueID, $notificationType]);
         }


      }
      // End of actual mail sending routine.
    }
  }

}

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

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