preview_site-1.1.2/src/PreviewSiteBuilder.php
src/PreviewSiteBuilder.php
<?php
namespace Drupal\preview_site;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Queue\DelayableQueueInterface;
use Drupal\Core\Queue\DelayedRequeueException;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Core\Queue\RequeueException;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\preview_site\Entity\PreviewSiteBuild;
use Drupal\preview_site\Entity\PreviewSiteBuildInterface;
use Drupal\preview_site\EventSubscribers\AdditionalPathsEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines a class for building a preview site.
*/
class PreviewSiteBuilder implements ContainerInjectionInterface {
/**
* Queue factory.
*
* @var \Drupal\Core\Queue\QueueFactory
*/
protected $queueFactory;
/**
* Queue manager.
*
* @var \Drupal\Core\Queue\QueueWorkerManagerInterface
*/
protected $queueManager;
/**
* State.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Entity-repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a new PreviewSiteBuilder.
*
* @param \Drupal\Core\Queue\QueueFactory $queueFactory
* Queue factory.
* @param \Drupal\Core\Queue\QueueWorkerManagerInterface $queueManager
* Queue manager.
* @param \Drupal\Core\State\StateInterface $state
* State.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository
* Entity-repository.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* Event dispatcher.
*/
public function __construct(QueueFactory $queueFactory, QueueWorkerManagerInterface $queueManager, StateInterface $state, EntityRepositoryInterface $entityRepository, protected ?EventDispatcherInterface $eventDispatcher) {
$this->queueFactory = $queueFactory;
$this->queueManager = $queueManager;
$this->state = $state;
$this->entityRepository = $entityRepository;
if (!$this->eventDispatcher) {
@trigger_error('Calling ' . __METHOD__ . ' without the $eventDispatcher argument is deprecated in preview_site:1.1.10 and will be required in preview_site:2.0.0. See https://www.drupal.org/node/3351768', E_USER_DEPRECATED);
$this->eventDispatcher = \Drupal::service('event_dispatcher');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('queue'),
$container->get('plugin.manager.queue_worker'),
$container->get('state'),
$container->get('entity.repository'),
$container->get('event_dispatcher'),
);
}
/**
* Returns a new instance of the preview site builder.
*
* @return \Drupal\preview_site\PreviewSiteBuilder
* Instance of the preview site builder.
*/
public static function factory() : PreviewSiteBuilder {
return \Drupal::classResolver(self::class);
}
/**
* Queues required tasks to generate a preview site.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site to generate.
*/
public function queueSiteGeneration(PreviewSiteBuildInterface $build) {
$asset_queue = $this->queueFactory->get('preview_site_assets:' . $build->id());
$generation_queue = $this->queueFactory->get('preview_site_generate:' . $build->id());
foreach ([$asset_queue, $generation_queue] as $queue) {
$queue->deleteQueue();
$queue->createQueue();
}
$build->queueGeneration($generation_queue);
}
/**
* Processes a single generation task.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site to generate.
*
* @return int
* Remaining items to process.
*/
public function processSiteGeneration(PreviewSiteBuildInterface $build) : int {
return $this->processQueueItem('preview_site_generate:' . $build->id());
}
/**
* Add additional paths to the processing queue.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site to generate.
*
* @return int
* Remaining items to process.
*/
public function queueAdditionalPaths(PreviewSiteBuildInterface $build): int {
$queue = $this->queueFactory->get('preview_site_assets:' . $build->id());
$count = $build->queueAdditionalPaths($queue);
$event = new AdditionalPathsEvent($build);
$this->eventDispatcher->dispatch($event);
$additional_paths = $event->getAdditionalPaths();
foreach ($additional_paths as $path) {
$queue->createItem($path);
}
return $count + count($additional_paths);
}
/**
* Processes a single asset generation task.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site to generate.
*
* @return int
* Remaining items to process.
*/
public function processAssetGeneration(PreviewSiteBuildInterface $build) : int {
return $this->processQueueItem('preview_site_assets:' . $build->id());
}
/**
* Queues required tasks to deploy a preview site.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site to deploy.
*/
public function queueSiteDeployment(PreviewSiteBuildInterface $build) {
$deploy_queue = $this->queueFactory->get('preview_site_deploy:' . $build->id());
$deploy_queue->deleteQueue();
$deploy_queue->createQueue();
$build->queueDeployment($deploy_queue);
}
/**
* Processes a single generation task.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site to generate.
*
* @return int
* Remaining items to process.
*/
public function processSiteDeployment(PreviewSiteBuildInterface $build) : int {
return $this->processQueueItem('preview_site_deploy:' . $build->id());
}
/**
* Processes a queue item.
*
* @param string $queue_name
* Queue name.
*
* @return int
* Number of remaining items.
*/
protected function processQueueItem(string $queue_name) : int {
$queue = $this->queueFactory->get($queue_name);
$item = $queue->claimItem();
if (!$item) {
return 0;
}
$queue->createQueue();
$worker = $this->queueManager->createInstance($queue_name);
try {
$worker->processItem($item->data);
$queue->deleteItem($item);
}
catch (DelayedRequeueException $e) {
if ($queue instanceof DelayableQueueInterface) {
$queue->delayItem($item, $e->getDelay());
return $queue->numberOfItems();
}
$queue->releaseItem($item);
}
catch (RequeueException $e) {
$queue->releaseItem($item);
}
return $queue->numberOfItems();
}
/**
* Gets a pre-build batch for building a preview-site.
*
* @param \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build
* Site build to generate.
*
* @return \Drupal\Core\Batch\BatchBuilder
* Batch built with tasks for this build.
*/
public function getPreviewSiteBuildBatch(PreviewSiteBuildInterface $build) : BatchBuilder {
return (new BatchBuilder())
->setTitle(new TranslatableMarkup('Building preview site'))
->setInitMessage(new TranslatableMarkup('Preparing...'))
->setProgressive(TRUE)
->setFinishCallback([self::class, 'finished'])
->setProgressMessage(new TranslatableMarkup('Step @current of @total'))
->addOperation([self::class, 'operationMarkDeploymentStarted'], [$build->id()])
->addOperation([self::class, 'operationQueueGenerate'], [$build->id()])
->addOperation([self::class, 'operationProcessGenerate'], [$build->id()])
->addOperation([self::class, 'operationQueueAdditionalPaths'], [$build->id()])
->addOperation([self::class, 'operationProcessAssets'], [$build->id()])
->addOperation([self::class, 'operationQueueDeploy'], [$build->id()])
->addOperation([self::class, 'operationProcessDeploy'], [$build->id()])
->addOperation([self::class, 'operationMarkDeploymentFinished'], [$build->id()]);
}
/**
* Batch callback.
*/
public static function operationMarkDeploymentStarted(int $build_id, array &$context) {
$context['results']['build_id'] = $build_id;
PreviewSiteBuild::load($build_id)->startDeployment(\Drupal::state());
$context['message'] = new TranslatableMarkup('Marked deployment as building');
}
/**
* Batch callback.
*/
public static function operationQueueGenerate(int $build_id) {
self::factory()->queueSiteGeneration(PreviewSiteBuild::load($build_id));
$context['message'] = new TranslatableMarkup('Queued content items for preview generation');
}
/**
* Batch callback.
*/
public static function operationProcessGenerate(int $build_id, array &$context) {
$remaining = self::factory()->processSiteGeneration(PreviewSiteBuild::load($build_id));
self::updateFinishedPercent($remaining, $context);
$context['results']['generated'] = ($context['results']['generated'] ?? 0) + 1;
$context['message'] = new PluralTranslatableMarkup($remaining, 'Generating previews for content items (1 item remaining)', 'Generating previews for content items (@count items remaining)');
}
/**
* Batch callback.
*/
public static function operationQueueAdditionalPaths(int $build_id, array &$context) {
$count = self::factory()->queueAdditionalPaths(PreviewSiteBuild::load($build_id));
$context['message'] = new PluralTranslatableMarkup($count, 'Queued one additional path', 'Queued @count additional paths');
}
/**
* Batch callback.
*/
public static function operationProcessAssets(int $build_id, array &$context) {
$remaining = self::factory()->processAssetGeneration(PreviewSiteBuild::load($build_id));
self::updateFinishedPercent($remaining, $context);
$context['results']['assets'] = ($context['results']['assets'] ?? 0) + 1;
$context['message'] = new PluralTranslatableMarkup($remaining, 'Generating assets for content items (1 item remaining)', 'Generating assets for content items (@count items remaining)');
}
/**
* Batch callback.
*/
public static function operationQueueDeploy(int $build_id) {
self::factory()->queueSiteDeployment(PreviewSiteBuild::load($build_id));
$context['message'] = new TranslatableMarkup('Queued artifacts for deployment');
}
/**
* Batch callback.
*/
public static function operationProcessDeploy(int $build_id, array &$context) {
$remaining = self::factory()->processSiteDeployment(PreviewSiteBuild::load($build_id));
self::updateFinishedPercent($remaining, $context);
$context['results']['deployed'] = ($context['results']['deployed'] ?? 0) + 1;
$context['message'] = new PluralTranslatableMarkup($remaining, 'Deploying artifacts (1 item remaining)', 'Deploying artifacts (@count items remaining)');
}
/**
* Batch callback.
*/
public static function operationMarkDeploymentFinished(int $build_id, array &$context) {
/** @var \Drupal\preview_site\Entity\PreviewSiteBuildInterface $build */
$build = PreviewSiteBuild::load($build_id);
if (!isset($context['sandbox']['clean_up_file_ids'])) {
$context['sandbox']['clean_up_file_ids'] = $build->finishDeployment(\Drupal::state())->getArtifactIds();
$context['sandbox']['total'] = count($context['sandbox']['clean_up_file_ids']);
}
if ($fids = array_splice($context['sandbox']['clean_up_file_ids'], 0, 10)) {
$file_storage = \Drupal::entityTypeManager()->getStorage('file');
$file_storage->delete($file_storage->loadMultiple($fids));
}
self::updateFinishedPercent(count($context['sandbox']['clean_up_file_ids']), $context);
if ($context['finished'] == 1) {
$status = $build->getStatus();
$context['message'] = new TranslatableMarkup('Marked deployment as @status', [
'@status' => $status,
]);
if ($status === PreviewSiteBuildInterface::STATUS_FAILED) {
$context['results']['generate_errors'] = TRUE;
}
return;
}
$context['message'] = new TranslatableMarkup('Deleting old artifacts');
}
/**
* Batch finished callback.
*/
public static function finished(bool $success, array $results, array $operations) {
if ($success && empty($results['generate_errors'])) {
\Drupal::messenger()->addMessage(new TranslatableMarkup('The preview site was successfully built, @generated previews were generated and @deployed artifacts were deployed.', [
'@generated' => $results['generated'],
'@deployed' => $results['deployed'],
]));
return;
}
if (!empty($results['generate_errors'])) {
\Drupal::messenger()->addError(new TranslatableMarkup('The preview site was not able to be built, preview generation failed.'));
return;
}
if (!empty($results['build_id'])) {
$build = PreviewSiteBuild::load($results['build_id']);
if ($build->getStatus() !== PreviewSiteBuildInterface::STATUS_FAILED) {
$build->deploymentFailed(\Drupal::state());
}
}
\Drupal::messenger()->addError(new PluralTranslatableMarkup(
count($operations),
'The preview site was unable to be generated and deployed, one operation failed.',
'The preview site was unable to be generated and deployed, @count operations failed.',
)
);
}
/**
* Updates finished percent.
*
* @param int $remaining
* Remaining items.
* @param array $context
* Batch context.
*/
protected static function updateFinishedPercent(int $remaining, array &$context): void {
if ($remaining === 0) {
$context['finished'] = 1;
return;
}
if (!isset($context['sandbox']['total'])) {
$context['sandbox']['total'] = $remaining + 1;
}
$context['finished'] = ($context['sandbox']['total'] - $remaining) / $context['sandbox']['total'];
}
/**
* {@inheritdoc}
*/
public function __sleep() {
// The queue factory can't be serialized.
return [];
}
/**
* Gets the active preview site build if one is running.
*/
public function getRunningBuild() : ?PreviewSiteBuildInterface {
if (($building = $this->state->get(PreviewSiteBuildInterface::BUILDING_STATE_KEY)) &&
($build = $this->entityRepository->loadEntityByUuid('preview_site_build', $building))) {
assert($build instanceof PreviewSiteBuildInterface);
return $build;
}
return NULL;
}
}
