tmgmt-8.x-1.x-dev/src/JobCheckoutManager.php
src/JobCheckoutManager.php
<?php
namespace Drupal\tmgmt;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides functionality related to job checkout and submissions.
*
* @ingroup tmgmt_job
*/
class JobCheckoutManager {
use StringTranslationTrait;
use MessengerTrait;
/**
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var \Drupal\tmgmt\JobQueue
*/
protected $jobQueue;
/**
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
public function __construct(RequestStack $request_stack, JobQueue $job_queue, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager) {
$this->requestStack = $request_stack;
$this->jobQueue = $job_queue;
$this->moduleHandler = $module_handler;
$this->configFactory = $config_factory;
$this->entityTypeManager = $entity_type_manager;
}
/**
* Attempts to checkout a number of jobs and prepare the necessary redirects.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state array, used to set the initial redirect.
* @param \Drupal\tmgmt\JobInterface[] $jobs
* Array of jobs to attempt checkout
*
* @ingroup tmgmt_job
*/
public function checkoutAndRedirect(FormStateInterface $form_state, array $jobs) {
$checkout_jobs = $this->checkoutMultiple($jobs);
$jobs_ready_for_checkout = array_udiff($jobs, $checkout_jobs, function (JobInterface $a, JobInterface $b) {
if ($a->id() < $b->id()) {
return -1;
}
elseif ($a->id() > $b->id()) {
return 1;
}
else {
return 0;
}
});
// If necessary, do a redirect.
if ($checkout_jobs || $jobs_ready_for_checkout) {
$request = $this->requestStack->getCurrentRequest();
if ($request->query->has('destination')) {
// Remove existing destination, as that will prevent us from being
// redirect to the job checkout page. Set the destination as the final
// redirect instead.
$redirect = $request->query->get('destination');
$request->query->remove('destination');
}
else {
$redirect = Url::fromRoute('<current>')->getInternalPath();
}
$this->jobQueue->startQueue(array_merge($checkout_jobs, $jobs_ready_for_checkout), $redirect);
// Prepare a batch job for the jobs that can be submitted already.
if ($jobs_ready_for_checkout) {
$batch = array(
'title' => t('Submitting jobs'),
'operations' => [],
'finished' => [JobCheckoutManager::class, 'batchSubmitFinished'],
);
foreach ($jobs_ready_for_checkout as $job) {
$batch['operations'][] = [
[JobCheckoutManager::class, 'batchSubmit'],
[$job->id(), NULL],
];
}
batch_set($batch);
}
else {
$form_state->setRedirectUrl($this->jobQueue->getNextUrl());
}
// Count of the job messages is one less due to the final redirect.
$this->messenger()->addStatus($this->getStringTranslation()->formatPlural(count($checkout_jobs), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
}
}
/**
* Attempts to check out a number of jobs.
*
* Performs a number of checks on each job and also allows to alter the
* behavior through hooks.
*
* @param \Drupal\tmgmt\JobInterface[] $jobs
* The jobs to be checked out.
* @param bool $skip_request_translation
* (optional) If TRUE, the jobs that can be submitted immediately will be
* prepared but not submitted yet. They will not be returned, the caller
* is responsible for submitting them.
*
* @return \Drupal\tmgmt\JobInterface[]
* List of jobs that have not been submitted immediately and need to be
* processed.
*
* @ingroup tmgmt_job
*
* @see \Drupal\tmgmt\JobCheckoutManager::checkoutAndRedirect()
*/
public function checkoutMultiple(array $jobs, $skip_request_translation = FALSE) {
$remaining_jobs = array();
// Allow other modules to jump in and eg. auto-checkout with rules or use a
// customized checkout form.
$this->moduleHandler->alter('tmgmt_job_checkout_before', $remaining_jobs, $jobs);
$denied = 0;
foreach ($jobs as $job) {
if (!$job->isUnprocessed()) {
// Job is already checked out, just ignore that one. This could happen
// if jobs have already been submitted in the before hook.
continue;
}
if (!$this->configFactory->get('tmgmt.settings')->get('quick_checkout') || $this->needsCheckoutForm($job)) {
if (!$job->access('submit')) {
// Ignore jobs if the user is not allowed to submit, ignore.
$denied++;
// Make sure that the job is saved.
$job->save();
continue;
}
$remaining_jobs[] = $job;
}
else {
// No manual checkout required. Request translations now, save the job
// in case someone excepts to be able to load the job and have the
// translator available.
$job->save();
if (!$skip_request_translation) {
$this->requestTranslation($job);
}
}
}
// Allow other modules to jump in and eg. auto-checkout with rules or use a
// customized checkout form.
$this->moduleHandler->alter('tmgmt_job_checkout_after', $remaining_jobs, $jobs);
// Display message for created jobs that can not be checked out.
if ($denied) {
$this->messenger()->addStatus($this->getStringTranslation()->formatPlural($denied, 'One job has been created.', '@count jobs have been created.'));
}
return $remaining_jobs;
}
/**
* Check if a job needs a checkout form.
*
* The current checks include if there is more than one translator available,
* if he has settings and if the job has a fixed target language.
*
* @param \Drupal\tmgmt\JobInterface $job
* The job item.
*
* @return bool
* TRUE if the job needs a checkout form.
*/
public function needsCheckoutForm(JobInterface $job) {
// If the job has no target language (or source language, even though this
// should never be the case in our use case), checkout is mandatory.
if (!$job->getTargetLangcode() || !$job->getSourceLangcode()) {
return TRUE;
}
// If no translator is pre-selected, try to pick one automatically.
if (!$job->hasTranslator()) {
// If there is more than a single translator available or if there are no
// translators available at all checkout is mandatory.
$translators = tmgmt_translator_load_available($job);
if (empty($translators) || count($translators) > 1) {
return TRUE;
}
$translator = reset($translators);
$job->translator = $translator->id();
}
// If that translator has settings, the checkout is mandatory.
if ($job->getTranslator()->hasCheckoutSettings($job)) {
return TRUE;
}
return FALSE;
}
/**
* Batch dispatch callback for submitting a job.
*
* @param int $job_id
* The job ID to submit.
* @param int|null $template_job_id
* (optional) A template job to use for the translator and settings.
*/
static public function batchSubmit($job_id, $template_job_id = NULL) {
\Drupal::service('tmgmt.job_checkout_manager')->doBatchSubmit($job_id, $template_job_id);
}
/**
* Batch callback for submitting a job.
*
* @param int $job_id
* The job ID to submit.
* @param int|null $template_job_id
* (optional) A template job to use for the translator and settings.
*/
public function doBatchSubmit($job_id, $template_job_id = NULL) {
/** @var \Drupal\tmgmt\JobInterface $job */
$job = $this->entityTypeManager->getStorage('tmgmt_job')->load($job_id);
if (!$job) {
return;
}
// Delete duplicates.
if ($existing_items_ids = $job->getConflictingItemIds()) {
$item_storage = $this->entityTypeManager->getStorage('tmgmt_job_item');
if (count($existing_items_ids) == $job->getItems()) {
$this->messenger()->addStatus($this->t('All job items for job @label are conflicting, the job can not be submitted.', ['@label' => $job->label()]));
return;
}
$item_storage->delete($item_storage->loadMultiple($existing_items_ids));
$num_of_items = count($existing_items_ids);
$this->messenger()->addWarning($this->getStringTranslation()->formatPlural($num_of_items, '1 conflicting item has been dropped for job @label.', '@count conflicting items have been dropped for job @label.', ['@label' => $job->label()]));
}
if ($template_job_id && $job_id != $template_job_id) {
/** @var \Drupal\tmgmt\JobInterface $template_job */
$template_job = $this->entityTypeManager->getStorage('tmgmt_job')->load($template_job_id);
if ($template_job) {
$job->set('translator', $template_job->getTranslatorId());
$job->set('settings', $template_job->get('settings')->getValue());
// If there is a custom label on the template job, copy that as well.
if ($template_job->get('label')->value) {
$job->set('label', $template_job->get('label')->value);
}
}
}
$translator = $job->getTranslator();
// Check translator availability.
$translatable_status = $translator->checkTranslatable($job);
if (!$translatable_status->getSuccess()) {
$this->messenger()->addError($this->t('Job @label is not translatable with the chosen settings: @reason', ['@label' => $job->label(), '@reason' => $translatable_status->getReason()]));
return;
}
if ($this->requestTranslation($job)) {
$this->jobQueue->markJobAsProcessed($job);
}
}
/**
* Batch dispatch submission finished callback.
*/
public static function batchSubmitFinished($success, $results, $operations) {
return \Drupal::service('tmgmt.job_checkout_manager')->doBatchSubmitFinished($success, $results, $operations);
}
/**
* Batch submission finished callback.
*/
public function doBatchSubmitFinished($success, $results, $operations) {
if ($redirect = $this->jobQueue->getNextUrl()) {
// Proceed to the next redirect queue item, if there is one.
return new RedirectResponse($redirect->setAbsolute()->toString());
}
elseif ($destination = $this->jobQueue->getDestination()) {
// Proceed to the defined destination if there is one.
return new RedirectResponse(Url::fromUri('base:' . $destination)->setAbsolute()->toString());
}
else {
// Per default we want to redirect the user to the overview.
return new RedirectResponse(Url::fromRoute('view.tmgmt_job_overview.page_1')->setAbsolute()->toString());
}
}
/**
* Requests translations for a job and prints messages which have happened since
* then.
*
* @param \Drupal\tmgmt\JobInterface $job
* The job object for which translations should be requested.
*
* @return bool
* TRUE if it worked, FALSE if there were any errors of the type error which
* means that something did go wrong.
*/
function requestTranslation(JobInterface $job) {
// Process the translation request.
$job->requestTranslation();
return tmgmt_write_request_messages($job);
}
/**
* Pass multiple jobs directly if supported by the translator.
*
* @param \Drupal\tmgmt\JobInterface[] $jobs
* Array of jobs.
* @param \Drupal\tmgmt\TranslatorInterface $translator
* Translator.
*
* @return void
*/
public function requestTranslationMultiple(array $jobs, TranslatorInterface|MultipleCheckoutInterface $translator) {
$unfinished_jobs = [];
$template_job = reset($jobs);
foreach ($jobs as $job) {
if ($template_job->id() != $job->id())
$job->set('translator', $template_job->getTranslatorId());
$job->set('settings', $template_job->get('settings')->getValue());
// If there is a custom label on the template job, copy that as well.
if ($template_job->get('label')->value) {
$job->set('label', $template_job->get('label')->value);
}
// Call the hook before requesting the translation.
// @see hook_tmgmt_job_before_request_translation()
$this->moduleHandler->invokeAll('tmgmt_job_before_request_translation', [$job]);
// We do not want to translate the items that are already translated,
// so we filter them.
if ($job->getCountPending() == 0) {
continue;
}
$job->setFilterTranslatedItems(TRUE);
$unfinished_jobs[] = $job;
}
$translator->getPlugin()->requestTranslationMultiple($unfinished_jobs);
foreach ($unfinished_jobs as $unfinished_job) {
tmgmt_write_request_messages($unfinished_job);
if (!$unfinished_job->isRejected()) {
$this->jobQueue->removeJobs([$unfinished_job->id()]);
}
$unfinished_job->setFilterTranslatedItems(FALSE);
// @see hook_tmgmt_job_after_request_translation()
$this->moduleHandler->invokeAll('tmgmt_job_after_request_translation', [$unfinished_job]);
}
}
}
