diff --git a/modules/purge_drush/composer.json b/modules/purge_drush/composer.json new file mode 100644 index 0000000..6314f1e --- /dev/null +++ b/modules/purge_drush/composer.json @@ -0,0 +1,17 @@ +{ + "name": "drupal/purge_drush", + "description": "Administrative Drush commands for Purge.", + "type": "drupal-module", + "license": "GPL-2.0+", + "require": { + "drupal/purge": "~3.0" + }, + "minimum-stability": "dev", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + } +} diff --git a/modules/purge_drush/drush.services.yml b/modules/purge_drush/drush.services.yml new file mode 100644 index 0000000..529525e --- /dev/null +++ b/modules/purge_drush/drush.services.yml @@ -0,0 +1,6 @@ +services: + purge_drush.commands: + class: \Drupal\purge_drush\Commands\PurgeDrushCommands + arguments: ['@purge.logger', '@purge.invalidation.factory', '@purge.processors', '@purge.purgers', '@purge.queue', '@purge.queue.stats', '@purge.queuers', '@purge.diagnostics'] + tags: + - { name: drush.command } diff --git a/modules/purge_drush/src/Commands/PurgeDrushCommands.php b/modules/purge_drush/src/Commands/PurgeDrushCommands.php new file mode 100644 index 0000000..2de1ec6 --- /dev/null +++ b/modules/purge_drush/src/Commands/PurgeDrushCommands.php @@ -0,0 +1,1135 @@ +purgeLogger = $purgeLogger; + $this->invalidationFactory = $invalidationFactory; + $this->processors = $processorsService; + $this->purgers = $purgersService; + $this->purgeQueue = $purgeQueue; + $this->queueStats = $queueStats; + $this->purgeQueuers = $purgeQueuers; + $this->diagnostics = $diagnostics; + } + + /** + * Disable debugging for all of Purge's log channels. + * + * @command p:debug:dis + * + * @usage drush p-debug-disable + * Disables the log channels. + * + * @aliases pddis,p-debug-dis,p-debug-disable + * + * @throws \LogicException + * + * @return array|void + */ + public function debugDis() { + $channels = function() { + $ids = []; + + foreach ($this->purgeLogger->getChannels() as $channel) { + if (in_array(RfcLogLevel::DEBUG, $channel['grants'])) { + $ids[] = $channel['id']; + } + } + + return $ids; + }; + + $disable = function() { + foreach ($this->purgeLogger->getChannels() as $channel) { + if (in_array(RfcLogLevel::DEBUG, $channel['grants'])) { + $key = array_search(RfcLogLevel::DEBUG, $channel['grants']); + unset($channel['grants'][$key]); + $this->purgeLogger->setChannel($channel['id'], $channel['grants']); + } + } + }; + + if (empty($channels())) { + $this->logger()->warning(dt('Debugging already disabled for all channels.')); + return; + } + + // Present the user with some help and a conformation message. + $this->output()->writeln('Disabling debug logging for the following log channels:'); + + foreach ($channels() as $id) { + $this->output()->writeln(' - ' . $id); + } + + $disable(); + $this->logger()->success(dt('Done!')); + } + + /** + * Enable debugging for all of Purge's log channels. + * + * @command p:debug:en + * + * @usage drush p-debug-enable + * Enables the log channels. + * + * @throws \LogicException + * @throws UserAbortException + * + * @aliases pden,p-debug-en,p-debug-enable + * + * @return array|void + */ + public function debugEn() { + $channels = function() { + $ids = []; + + foreach ($this->purgeLogger->getChannels() as $channel) { + if (!in_array(RfcLogLevel::DEBUG, $channel['grants'])) { + $ids[] = $channel['id']; + } + } + + return $ids; + }; + + $enable = function() { + foreach ($this->purgeLogger->getChannels() as $channel) { + if (!in_array(RfcLogLevel::DEBUG, $channel['grants'])) { + $channel['grants'][] = RfcLogLevel::DEBUG; + $this->purgeLogger->setChannel($channel['id'], $channel['grants']); + } + } + }; + + if (empty($channels())) { + $this->logger()->warning(dt('Debugging already enabled for all channels.')); + return; + } + + // Present the user with some help and a conformation message. + $this->output()->writeln('About to enable debugging for the following log channels:'); + + foreach ($channels() as $id) { + $this->output()->writeln(' - ' . $id); + } + + $this->output()->writeln("\nOnce enabled, this allows you to run Drush commands like" + . ' p-queue-work with the -v parameter, giving you a detailed' + . ' amount of live-debugging information getting logged by Purge' + . ' and modules integrating with it.' + . ' HOWEVER, debug logging is VERY verbose and can add' + . ' millions of messages when left enabled for too long. NEVER' + . ' enable this on a production environment without fully' + . " understanding the consequences!\n" + ); + + if ($this->io()->confirm('Are you sure you want to enable it?')) { + $enable(); + $this->logger()->success("Enabled! Use p-debug-dis to disable when you're finished!"); + } + else { + throw new UserAbortException(); + } + } + + /** + * Generate a diagnostic self-service report. + * + * @command p:diagnostics + * + * @usage drush p-diagnostics + * Build the diagnostic report as a table. + * @usage drush p-diagnostics --format=json + * Export as JSON. + * @usage drush p-diagnostics --format=yaml + * Export as YAML. + * + * @aliases pdia,p-diagnostics + * + * @return array + */ + public function diagnostics() { + $output = []; + + foreach ($this->diagnostics as $check) { + $output[] = [ + 'id' => (string) $check->getPluginId(), + 'title' => (string) $check->getTitle(), + 'value' => (string) $check->getValue(), + 'severity_int' => $check->getSeverity(), + 'severity' => (string) $check->getSeverityString(), + 'description' => (string) $check->getDescription(), + 'recommendation' => (string) $check->getRecommendation(), + 'blocks_processing' => $check->getSeverity() === DiagnosticCheckInterface::SEVERITY_ERROR, + ]; + } + + return $output; + } + + /** + * Directly invalidate an item without going through the queue. + * + * @command p:invalidate + * + * @param $type + * The type of invalidation to perform, e.g.: tag, path, url. + * @param $expression + * The string expression of what needs to be invalidated. + * + * @usage drush p-invalidate tag node:1 + * Clears URLs tagged with "node:1" from external caching platforms. + * @usage drush p-invalidate url http://www.drupal.org/ + * Clears "http://www.drupal.org/" from external caching platforms. + * @usage drush p-invalidate everything + * Clears everything on external caching platforms. + * + * @aliases pinv,p-invalidate + * + * @throws \Exception + * When a drush error occurs. + * @throws UserAbortException + * When the user aborts the command. + * + * @return void + */ + public function invalidate($type, $expression = NULL) { + // Retrieve our queuer object and fail when it is not returned. + if (!($processor = $this->processors->get('drush_purge_invalidate'))) { + throw new \Exception(dt('Not authorized, processor missing!')); + } + + // Instantiate the invalidation object based on user input. + try { + $invalidations = [$this->invalidationFactory->get($type, $expression)]; + } + catch (PluginNotFoundException $e) { + throw new \Exception(dt("Type '@type' does not exist, see 'drush p-types' for available types.", ['@type' => $type])); + } + catch (TypeUnsupportedException $e) { + throw new \Exception(dt("There is no purger supporting '@type', please install one!", ['@type' => $type])); + } + catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + + // Prevent users from accidentally harming their website. + if ($type === 'everything') { + $this->output()->writeln('Invalidating everything will mass-clear potentially thousands' + . ' of pages, which could temporarily make your site really slow as' + . " external caches will have to warm up again.\n"); + if (!$this->io()->confirm('Are you really sure?')) { + throw new UserAbortException(); + } + } + + // Attempt the cache invalidation. Exceptions will be thrown when errors + // occur. + try { + $this->purgers->invalidate($processor, $invalidations); + } + catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + + // Since this command is more meant for testing, we only regard SUCCEEDED as + // a acceptable return state to call success on. + if ($invalidations[0]->getState() === InvStatesInterface::SUCCEEDED) { + $this->logger()->success(dt('Item invalidated successfully!')); + return; + } + + $this->logger()->error(dt('Invalidation failed, its return state is: @state.', [ + '@state' => $invalidations[0]->getStateString() + ])); + } + + /** + * Add a new processor. + * + * @command p:processor:add + * + * @param $id + * The plugin ID of the processor to add. + * + * @usage drush p-processor-add ID + * Add a processor of type ID. + * + * @aliases pradd,p-processor-add + * + * @throws \Exception + * When a drush error occurs. + * + * @return void + */ + public function processorAdd($id) { + $enabled = $this->processors->getPluginsEnabled(); + + // Verify that the plugin exists. + if (!isset($this->processors->getPlugins()[$id])) { + throw new \Exception(dt('The given plugin does not exist!')); + } + + // Verify that the plugin is available and thus not yet enabled. + if (!in_array($id, $this->processors->getPluginsAvailable())) { + $this->logger()->warning(dt('This processor is already enabled!')); + return; + } + + // Define the new instance and store it. + $enabled[] = $id; + $this->processors->setPluginsEnabled($enabled); + $this->logger()->success(dt('The processor has been added!')); + } + + /** + * List all enabled processors. + * + * @command p:processor:ls + * + * @usage drush p-processor-ls + * List all processors in a table. + * + * @aliases prls,p-processor-ls + * + * @return array + */ + public function processorLs() { + $output = []; + + foreach ($this->processors as $processor) { + $plugin_id = $processor->getPluginId(); + $output[$plugin_id] = [ + 'id' => $plugin_id, + ]; + } + + return $output; + } + + /** + * List available processor plugin IDs that can be added. + * + * @command p:processor:lsa + * + * @usage drush p-processor-lsa + * List available plugin IDs for which processors can be created. + * + * @aliases prlsa,p-processor-lsa + * + * @return array + */ + public function processorLsa() { + $available = $this->processors->getPluginsAvailable(); + $output = []; + + foreach ($available as $plugin_id) { + $output[$plugin_id] = [ + 'plugin_id' => $plugin_id, + ]; + } + + return $output; + } + + /** + * Remove a processor. + * + * @command p:processor:rm + * + * @param $id + * The plugin ID of the processor to remove. + * + * @usage drush p-processor-rm ID + * Remove the given processor. + * + * @aliases prrm,p-processor-rm + * + * @throws \Exception + * When a drush error occurs. + */ + public function processorRm($id) { + $enabled = $this->processors->getPluginsEnabled(); + + // Verify that the processor exists. + if (!in_array($id, $enabled)) { + throw new \Exception(dt('The given plugin ID is not valid!')); + } + + // Remove the processor and finish command execution. + unset($enabled[array_search($id, $enabled)]); + $this->processors->setPluginsEnabled($enabled); + + $this->logger()->success(dt('The processor has been removed!')); + } + + /** + * Create a new purger instance. + * + * @command p:purger:add + * + * @param $id + * The plugin ID of the purger instance to create. + * @param array $options + * An associative array of options whose values come from cli, aliases, + * config, etc. + * + * @option if-not-exists Don't create a new purger if one of this type exists. + * + * @usage drush p-purger-add ID + * Add a purger of type ID. + * @usage drush p-purger-add --if-not-exists ID + * Create purger ID if it does not exist. + * + * @aliases ppadd,p-purger-add + * + * @throws \Exception + * When a drush error occurs. + * + * @return void + */ + public function purgerAdd($id, array $options = ['if-not-exists' => NULL]) { + $enabled = $this->purgers->getPluginsEnabled(); + + // Verify that the plugin exists. + if (!isset($this->purgers->getPlugins()[$id])) { + throw new \Exception(dt('The given plugin does not exist!')); + } + + // When --if-not-exists is passed, we cancel creating double purgers. + if ($options['if-not-exists']) { + if (in_array($id, $enabled)) { + $this->logger()->warning(dt('The purger already exists!')); + return; + } + } + + // Verify that new instances of the plugin may be created. + if (!in_array($id, $this->purgers->getPluginsAvailable())) { + throw new \Exception(dt('No more instances of this plugin can be created!')); + } + + // Define the new instance and store it. + $enabled[$this->purgers->createId()] = $id; + $this->purgers->setPluginsEnabled($enabled); + + $this->logger()->success(dt('The purger has been created!')); + } + + /** + * List all configured purgers in order of execution. + * + * @command p:purger:ls + * + * @usage drush p-purger-ls + * List all configured purgers in order of execution. + * + * @aliases ppls,p-purger-ls + * + * @return array + */ + public function purgerLs() { + $enabled = $this->purgers->getPluginsEnabled(); + $output = []; + + if (!empty($enabled)) { + $output['headers'] = [ + 'instance_id' => '' . dt('Instance ID') . '', + 'plugin_id' => '' . dt('Plugin ID') . '', + ]; + } + + foreach ($enabled as $instance_id => $plugin_id) { + $output[$instance_id] = [ + 'instance_id' => $instance_id, + 'plugin_id' => $plugin_id, + ]; + } + + return $output; + } + + /** + * List available plugin IDs for which purgers can be added. + * + * @command p:purger:lsa + * + * @usage drush p-purger-lsa + * List available plugin IDs for which purgers can be created. + * + * @aliases pplsa,p-purger-lsa + * + * @return array + */ + public function purgerLsa() { + $available = $this->purgers->getPluginsAvailable(); + $output = []; + + foreach ($available as $plugin_id) { + $output[$plugin_id] = [ + 'plugin_id' => $plugin_id, + ]; + } + + return $output; + } + + /** + * Move the given purger DOWN in the execution order. + * + * @command p:purger:mvd + * + * @param $id + * The instance ID of the purger to move down. + * + * @usage drush p-purger-mv-down ID + * Move this purger down. + * + * @aliases ppmvd,p-purger-mvd,p-purger-mv-down + * + * @throws \Exception + * When a drush error occurs. + * + * @return bool|string + */ + public function purgerMvd($id) { + $enabled = $this->purgers->getPluginsEnabled(); + + // Verify that the purger instance exists. + if (!isset($enabled[$id])) { + throw new \Exception(dt('The given instance ID is not valid!')); + } + + // Move the purger down and finish command execution. + $this->purgers->movePurgerDown($id); + + $this->logger()->success(dt('The purger moved one place down!')); + } + + /** + * Move the given purger UP in the execution order. + * + * @command p:purger:mvu + * + * @param $id + * The instance ID of the purger to move up. + * + * @usage drush p-purger-mv-up ID + * Move this purger up. + * + * @aliases ppmvu,p-purger-mvu,p-purger-mv-up + * + * @throws \Exception + * When a drush error occurs. + * + * @return bool|string + */ + public function purgerMvu($id) { + $enabled = $this->purgers->getPluginsEnabled(); + + // Verify that the purger instance exists. + if (!isset($enabled[$id])) { + throw new \Exception(dt('The given instance ID is not valid!')); + } + + // Move the purger up and finish command execution. + $this->purgers->movePurgerUp($id); + + $this->logger()->success(dt('The purger moved one place up!')); + } + + /** + * Remove a purger instance. + * + * @command p:purger:rm + * + * @param $id + * The instance ID of the purger to remove. + * + * @usage drush p-purger-rm ID + * Remove the given purger. + * + * @aliases pprm,p-purger-rm + * + * @throws \Exception + * When a drush error occurs. + * + * @return bool|string + */ + public function purgerRm($id) { + $enabled = $this->purgers->getPluginsEnabled(); + + // Verify that the purger instance exists. + if (!isset($enabled[$id])) { + throw new \Exception(dt('The given instance ID is not valid!')); + } + + // Remove the purger instance and finish command execution. + unset($enabled[$id]); + $this->purgers->setPluginsEnabled($enabled); + + $this->logger()->success(dt('The purger has been removed!')); + } + + /** + * Add one or more items to the queue for later processing. + * + * @command p:queue:add + * + * @param string ... + * Parameters are expected to be in the format " " + * and can contain commas to separate extra items, in the same format. + * - Type: The type of invalidation to queue, e.g.: tag, path, url. + * - Expression: The string expression of what needs to be invalidated. + * + * @usage drush p-queue-add "tag node:1" + * Clears all cached pages matching TAG "node:1". + * @usage drush pqa "url http://www.s.com/rss.xml" + * Clears only the URL provided. + * @usage drush pqa "wildcardurl http://s.com/f/*" + * Clears URLs by wildcard, all under http://s.com/f/ will be cleared. + * @usage drush pqa everything + * Instructs to clear the entire site, be careful! + * @usage drush pqa tag node:1,tag node:2,url http://../rss.xml,tag node:321 + * Comma separated input of multiple items. + * + * @aliases pqa,p-queue-add + * + * @throws \Exception + * When a drush error occurs. + */ + public function queueAdd() { + // Retrieve our queuer object and fail when it is not returned. + if (!$queuer = $this->purgeQueuers->get('drush_purge_queue_add')) { + throw new \Exception(dt('Not authorized, queuer missing!')); + } + + // Clean input and parse comma-separated input items. + $items = func_get_args(); + array_pop($items); + $items = array_map('trim', explode(',', implode(' ', $items))); + + array_walk($items, function(&$value, $key) { + $value = explode(' ', $value); + + if (!isset($value[1])) { + $value[1] = NULL; + } + }); + + // Iterate the provided input and provide feedback to the user. + $invalidations = []; + + foreach ($items as $i => list($type, $expression)) { + if (NULL === $type || empty($type)) { + unset($items[$i]); + continue; + } + + // Instantiate the invalidation object based on user input. + try { + $invalidations[] = $this->invalidationFactory->get($type, $expression); + } + catch (PluginNotFoundException $e) { + throw new \Exception(dt("Type '@type' does not exist, see 'drush p-types' for available types.", ['@type' => $type])); + } + catch (TypeUnsupportedException $e) { + throw new \Exception(dt("There is no purger supporting '@type', please install one!", ['@type' => $type])); + } + catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + + // Prevent users from accidentally harming their website. + if ($type === 'everything') { + $this->output()->writeln('Invalidating everything will mass-clear potentially' + . ' thousands of pages, which could temporarily make your site really' + . " slow as external caches will have to warm up again.\n"); + + if (!$this->io()->confirm('Are you really sure?')) { + throw new UserAbortException(); + } + } + } + + // Add the objects to the queue and give user feedback. + $this->purgeQueue->add($queuer, $invalidations); + + $this->logger()->success(dt('Added @count item(s) to the queue.', ['@count' => count($invalidations)])); + } + + /** + * Inspect what is in the queue by paging through it. + * + * @command p:queue:browse + * + * @param array $options + * An associative array of options whose values come from cli, aliases, config, etc. + * + * @option limit + * The number of items to show on a single page. + * @option page + * The page to show data for, pages start at 1. + * + * @usage drush p-queue-browse + * Browse queue content and press space to load more. + * @usage drush p-queue-browse --limit=30 + * Browse the queue content and show 30 items at a time. + * @usage drush p-queue-browse --page=3 + * Show page 3 of the queue. + * + * @aliases pqb,p-queue-browse + * + * @throws \Exception + * When a drush error occurs. + * + * @return array + * + * @todo Add browse functionality, see Drush 8 implementation. + */ + public function queueBrowse(array $options = ['limit' => 30, 'page' => 1]) { + $output = []; + + foreach ($this->purgeQueue->selectPage((int) $options['page']) as $immutable) { + $exp = $immutable->getExpression(); + $output[$exp][] = $exp; + } + + return $output; + } + + /** + * Empty the entire queue. + * + * @command p:queue:empty + * + * @usage drush p-queue-empty + * Empty the entire queue. + * + * @aliases pqe,p-queue-empty + */ + public function queueEmpty() { + $total = (int) $this->queueStats->numberOfItems()->get(); + $this->purgeQueue->emptyQueue(); + + if ($total !== 0) { + $this->logger()->success(dt('Cleared @total items from the queue.', ['@total' => $total])); + return; + } + + $this->logger()->notice(dt('The queue was empty, nothing to clear!')); + } + + /** + * Retrieve the queue statistics. + * + * @command p:queue:stats + * + * @param array $options + * An associative array of options whose values come from cli, aliases, + * config, etc. + * + * @option reset-totals + * Wipe the TOTAL statistical counters. + * + * @usage drush p-queue-stats + * Retrieve the queue statistics. + * @usage drush p-queue-stats --reset-totals + * Wipe the TOTAL statistical counters. + * + * @aliases pqs,p-queue-stats + * + * @throws UserAbortException + * + * @return array + */ + public function queueStats(array $options = ['reset-totals' => NULL]) { + // Reset the total counters if requested to. + if ($options['reset-totals']) { + $this->output()->writeln("You are about to reset all total counters...\n"); + + if ($this->io()->confirm('Are you really sure?')) { + $this->queueStats->resetTotals(); + $this->logger()->success('Done!'); + return; + } + else { + throw new UserAbortException(); + } + } + + // Normal output generation. + $table = []; + $align_right = function($input, $size = 20) { + $this->output()->writeln(str_repeat(' ', $size - strlen($input)) . $input); + }; + + foreach ($this->queueStats as $statistic) { + $table[] = [ + 'left' => $align_right(strtoupper($statistic->getId())), + 'right' => $statistic->getTitle() + ]; + $table[] = [ + 'left' => $align_right($statistic->getInteger()), + 'right' => '', + ]; + $table[] = ['left' => '', 'right' => $statistic->getDescription()]; + $table[] = ['left' => '', 'right' => '']; + } + + return $table; + } + + /** + * Count how many items currently sit in the queue. + * + * @command p:queue:volume + * + * @usage drush p-queue-volume + * The number of items in the queue. + * + * @aliases pqv,p-queue-volume + */ + public function queueVolume() { + $volume = (int) $this->purgeQueue->numberOfItems(); + $this->output()->writeln(dt('There are @total items in the queue.', ['@total' => $volume])); + } + + /** + * Claim a chunk of items from the queue and process them. + * + * @command p:queue:work + * + * @param array $options + * An associative array of options whose values come from cli, aliases, + * config, etc. + * + * @option finish + * Continue processing till the queue is empty. + * + * @usage drush p-queue-work + * Claim a chunk of items from the queue and process them. + * + * @aliases pqw,p-queue-work + * + * @throws \Exception + * When a drush error occurs. + */ + public function queueWork(array $options = ['finish' => NULL]) { + // Retrieve our queuer object and fail when it is not returned. + if (!($processor = $this->processors->get('drush_purge_queue_work'))) { + throw new \Exception('Not authorized, processor missing!'); + } + + // In finish mode, we'll fork ourselves until the entire queue is empty. + if ($options['finish']) { + if ($this->purgeQueue->numberOfItems() < 1) { + throw new \Exception('No items can be claimed from the queue.'); + } + + // Create the arguments list. Silence subprocesses in boolean mode. + $arguments = ['@self', 'p-queue-work', [], ['format' => $options['format']]]; + + // Iterate until the queue is empty and collect return values. + $returns = []; + + while ($this->purgeQueue->numberOfItems() > 0 && !in_array(FALSE, $returns, TRUE)) { + $cmd = drush_invoke_process(...$arguments); + + if ($cmd['error_status']) { + $cmd['object'] = FALSE; + } + + if (is_array($cmd['object']) && empty($cmd['object'])) { + $cmd['object'] = FALSE; + } + + $returns[] = $cmd['object']; + } + + $this->logger()->success('Finished.'); + } + + // Single chunk processing mode. + else { + $claims = $this->purgeQueue->claim(); + // Attempt the cache invalidation and deal with errors. + try { + $this->purgers->invalidate($processor, $claims); + } + catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + finally { + $this->purgeQueue->handleResults($claims); + } + + // Evaluate all claim states to booleans and collect the results. Then + // return the overall outcome, which is FALSE if one failed. + $results = []; + + foreach ($claims as $claim) { + if (in_array($claim->getStateString(), ['PROCESSING', 'SUCCEEDED'])) { + $results['success'][] = TRUE; + } + else { + $results['error'][] = TRUE; + } + } + + if (isset($results['success'])) { + $this->logger()->success('Processed ' . count($results['success']) . ' objects.'); + } + + if (isset($results['error'])) { + $this->logger()->error('Failed to process ' . count($results['error']) . ' objects.'); + } + } + } + + /** + * Add a new queuer. + * + * @command p:queuer:add + * + * @param $id + * The plugin ID of the queuer to add. + * + * @usage drush p-queuer-add ID + * Add a queuer of type ID. + * + * @aliases puadd,p-queuer-add + * + * @throws \Exception + * When a drush error occurs. + */ + public function queuerAdd($id) { + $enabled = $this->purgeQueuers->getPluginsEnabled(); + + // Verify that the plugin exists. + if (!isset($this->purgeQueuers->getPlugins()[$id])) { + throw new \Exception('The given plugin does not exist!'); + } + + // Verify that the plugin is available and thus not yet enabled. + if (!in_array($id, $this->purgeQueuers->getPluginsAvailable())) { + $this->logger()->warning(dt('This queuer is already enabled!')); + return; + } + + // Define the new instance and store it. + $enabled[] = $id; + $this->purgeQueuers->setPluginsEnabled($enabled); + + $this->logger()->success(dt('The queuer has been added!')); + } + + /** + * List all enabled queuers. + * + * @command p:queuer:ls + * + * @usage drush p-queuer-ls + * List all queuers in a table. + * + * @aliases puls,p-queuer-ls + * + * @return array + */ + public function queuerLs() { + $output = []; + + foreach ($this->purgeQueuers as $queuer) { + $output[]['id'] = $queuer->getPluginId(); + } + + return $output; + } + + /** + * List available queuer plugin IDs that can be added. + * + * @command p:queuer:lsa + * + * @usage drush p-queuer-lsa + * List available plugin IDs for which queuers can be created. + * + * @aliases pulsa,p-queuer-lsa + * + * @return array + */ + public function queuerLsa() { + $available = $this->purgeQueuers->getPluginsAvailable(); + $output = []; + + foreach ($available as $plugin_id) { + $output[$plugin_id]['plugin_id'] = $plugin_id; + } + + return $output; + } + + /** + * Remove a queuer. + * + * @command p:queuer:rm + * + * @param $id + * The plugin ID of the queuer to remove. + * + * @usage drush p-queuer-rm ID + * Remove the given queuer. + * + * @aliases purm,p-queuer-rm + * + * @throws \Exception + * When a drush error occurs. + */ + public function queuerRm($id) { + $enabled = $this->purgeQueuers->getPluginsEnabled(); + + // Verify that the queuer exists. + if (!in_array($id, $enabled)) { + throw new \Exception('The given plugin ID is not valid!'); + } + + // Remove the queuer and finish command execution. + unset($enabled[array_search($id, $enabled)]); + $this->purgeQueuers->setPluginsEnabled($enabled); + + $this->logger()->success(dt('The queuer has been removed!')); + } + + /** + * List all supported cache invalidation types. + * + * @command p:types + * + * @usage drush p-types + * List all supported cache invalidation types. + * + * @aliases ptyp,p-types + * + * @return array + */ + public function types() { + $output = []; + + // Return a simple listing of supported types. + foreach ($this->purgers->getTypes() as $type) { + $output[$type]['type'] = $type; + } + + return $output; + } + + /** + * PurgeDrushCommands destructor. + * + * Drush 9 is not triggering the PostResponseEvent which should execute the + * destruct method of the objects called below. In awaiting of a decent + * solution, we do it manually. + * + * @todo Remove this method as soon as these services are destructable using + * the KernelDestructionSubscriber. + * + * @see https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21EventSubscriber%21KernelDestructionSubscriber.php/class/KernelDestructionSubscriber/8.5.x + */ + public function __destruct() { + $this->purgeLogger->destruct(); + $this->purgeQueue->destruct(); + $this->queueStats->destruct(); + } + +}