diff --git a/examples.module b/examples.module index 7b816b0..9c9b091 100644 --- a/examples.module +++ b/examples.module @@ -53,6 +53,7 @@ function examples_toolbar() { 'phpunit_example' => 'phpunit_example_description', 'plugin_type_example' => 'plugin_type_example.description', 'simpletest_example' => 'simpletest_example_description', + 'queue_example' => 'queue_example', 'tablesort_example' => 'tablesort_example_description', 'tour_example' => 'tour_example_description', ); diff --git a/queue_example/queue_example.info.yml b/queue_example/queue_example.info.yml new file mode 100644 index 0000000..e7d94ff --- /dev/null +++ b/queue_example/queue_example.info.yml @@ -0,0 +1,5 @@ +name: Queue example +type: module +description: Examples of using the Drupal Queue API. +package: Example modules +core: 8.x diff --git a/queue_example/queue_example.links.menu.yml b/queue_example/queue_example.links.menu.yml new file mode 100644 index 0000000..29b8614 --- /dev/null +++ b/queue_example/queue_example.links.menu.yml @@ -0,0 +1,3 @@ +queue_example: + title: Queue Example + route_name: queue_example diff --git a/queue_example/queue_example.module b/queue_example/queue_example.module new file mode 100644 index 0000000..891c93c --- /dev/null +++ b/queue_example/queue_example.module @@ -0,0 +1,37 @@ +queueFactory = $queue_factory; + $this->database = $database; + $this->cron = $cron; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('queue'), $container->get('database'), $container->get('cron')); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + // Return a string that is the unique ID of our form. Best practice here is + // to namespace the form based on your module's name. + return 'queue_example'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Simple counter that makes it possible to put auto-incrementing default + // string into the string to insert. + if (empty($form_state->get('insert_counter'))) { + $form_state->set('insert_counter', 1); + } + + $queue_name = $form_state->getValue('queue_name') ?: 'queue_example_first_queue'; + $items = $this->retrieveQueue($queue_name); + + $form['help'] = array( + '#type' => 'markup', + '#markup' => '
' . $this->t('This page is an interface on the Drupal queue API. You can add new items to the queue, "claim" one (retrieve the next item and keep a lock on it), and delete one (remove it from the queue). Note that claims are not expired until cron runs, so there is a special button to run cron to perform any necessary expirations.') . '
', + ); + + $queue_names = array('queue_example_first_queue', 'queue_example_second_queue'); + $form['queue_name'] = array( + '#type' => 'select', + '#title' => $this->t('Choose queue to examine'), + '#options' => array_combine($queue_names, $queue_names), + '#default_value' => $queue_name, + ); + + $form['queue_show'] = array( + '#type' => 'submit', + '#value' => $this->t('Show queue'), + '#submit' => array(array($this, 'submitShowQueue')), + ); + + $form['status_fieldset'] = array( + '#type' => 'fieldset', + '#title' => $this->t('Queue status for @name', array('@name' => $queue_name)), + '#collapsible' => TRUE, + ); + + if (count($items) > 0) { + $form['status_fieldset']['status'] = array( + '#theme' => 'table', + '#header' => array( + t('Item ID'), + t('Claimed/Expiration'), + t('Created'), + t('Content/Data'), + ), + '#rows' => array_map(array($this, 'processQueueItemForTable'), $items), + ); + } + else { + $form['status_fieldset']['status'] = array( + '#type' => 'markup', + '#markup' => $this->t('There are no items in the queue.'), + ); + } + + $form['insert_fieldset'] = array( + '#type' => 'fieldset', + '#title' => $this->t('Insert into @name', array('@name' => $queue_name)), + ); + + $form['insert_fieldset']['string_to_add'] = array( + '#type' => 'textfield', + '#size' => 10, + '#default_value' => $this->t('item @counter', array('@counter' => $form_state->get('insert_counter'))), + ); + + $form['insert_fieldset']['add_item'] = array( + '#type' => 'submit', + '#value' => $this->t('Insert into queue'), + '#submit' => array(array($this, 'submitAddQueueItem')), + ); + + $form['claim_fieldset'] = array( + '#type' => 'fieldset', + '#title' => $this->t('Claim from queue'), + '#collapsible' => TRUE, + ); + + $form['claim_fieldset']['claim_time'] = array( + '#type' => 'radios', + '#title' => $this->t('Claim time, in seconds'), + '#options' => array( + 0 => $this->t('none'), + 5 => $this->t('5 seconds'), + 60 => $this->t('60 seconds'), + ), + '#description' => $this->t('This time is only valid if cron runs during this time period. You can run cron manually below.'), + '#default_value' => $form_state->getValue('claim_time') ?: 5, + ); + + $form['claim_fieldset']['claim_item'] = array( + '#type' => 'submit', + '#value' => $this->t('Claim the next item from the queue'), + '#submit' => array(array($this, 'submitClaimItem')), + ); + + $form['claim_fieldset']['claim_and_delete_item'] = array( + '#type' => 'submit', + '#value' => $this->t('Claim the next item and delete it'), + '#submit' => array(array($this, 'submitClaimDeleteItem')), + ); + + $form['claim_fieldset']['run_cron'] = array( + '#type' => 'submit', + '#value' => $this->t('Run cron manually to expire claims'), + '#submit' => array(array($this, 'submitRunCron')), + ); + + $form['delete_queue'] = array( + '#type' => 'submit', + '#value' => $this->t('Delete the queue and items in it'), + '#submit' => array(array($this, 'submitDeleteQueue')), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + + /** + * Retrieves the queue from the database for display purposes only. + * + * It is not recommended to access the database directly, and this is only + * here so that the user interface can give a good idea of what's going on in the + * queue. + * + * @param $queue_name + * The name of the queue from which to fetch items. + * + * @return array + */ + public function retrieveQueue($queue_name) { + $items = array(); + + $result = $this->database->query('SELECT item_id, data, expire, created FROM queue WHERE name = :name ORDER BY item_id', + array(':name' => $queue_name), + array('fetch' => \PDO::FETCH_ASSOC)); + foreach ($result as $item) { + $items[] = $item; + } + + return $items; + } + + /** + * Submit function for the show-queue button. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function submitShowQueue(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + + // Get the number of items. + $count = $queue->numberOfItems(); + + // Update the form item counter. + $form_state->set('insert_counter', $count + 1); + + // Unset the string_to_add textbox. + $form_state->unsetValue('string_to_add'); + + $form_state->setRebuild(); + } + + /** + * Submit function for the insert-into-queue button. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function submitAddQueueItem(array &$form, FormStateInterface $form_state) { + // Get a queue (of the default type) called 'queue_example_queue'. + // If the default queue class is SystemQueue this creates a queue that + // stores its items in the database. + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + + // Queue the string. + $queue->createItem($form_state->getValue('string_to_add')); + $count = $queue->numberOfItems(); + drupal_set_message($this->t('Queued your string (@string_to_add). There are now @count items in the queue.', array('@count' => $count, '@string_to_add' => $form_state->getValue('string_to_add')))); + // Allows us to keep information in $form_state. + $form_state->setRebuild(); + + // Unsetting the string_to_add allows us to set the incremented default + // value for the user so they don't have to type anything. + $form_state->unsetValue('string_to_add'); + $form_state->set('insert_counter', $count + 1); + } + + /** + * Submit function for the "claim" button. + * Claims (retrieves) an item from the queue and reports the results. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function submitClaimItem(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + $item = $queue->claimItem($form_state->getValue('claim_time')); + $count = $queue->numberOfItems(); + if (!empty($item)) { + drupal_set_message($this->t('Claimed item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.', array('@count' => $count, '@item_id' => $item->item_id, '@string' => $item->data, '@seconds' => $form_state->getValue('claim_time')))); + } + else { + drupal_set_message($this->t('There were no items in the queue available to claim. There are @count items in the queue.', array('@count' => $count))); + } + $form_state->setRebuild(); + } + + /** + * Submit function for "Claim and delete" button. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function submitClaimDeleteItem(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + $count = $queue->numberOfItems(); + $item = $queue->claimItem(60); + if (!empty($item)) { + drupal_set_message($this->t('Claimed and deleted item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.', array( + '@count' => $count, + '@item_id' => $item->item_id, + '@string' => $item->data, + '@seconds' => $form_state->getValue('claim_time'), + ))); + $queue->deleteItem($item); + $count = $queue->numberOfItems(); + drupal_set_message($this->t('There are now @count items in the queue.', array('@count' => $count))); + } + else { + $count = $queue->numberOfItems(); + drupal_set_message($this->t('There were no items in the queue available to claim/delete. There are currently @count items in the queue.', array('@count' => $count))); + } + $form_state->setRebuild(); + } + + /** + * Submit function for "run cron" button. + * + * Runs cron (to release expired claims) and reports the results. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function submitRunCron(array &$form, FormStateInterface $form_state) { + $this->cron->run(); + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // @see https://www.drupal.org/node/2705809 + if ($queue instanceof QueueGarbageCollectionInterface) { + $queue->garbageCollection(); + } + // There is no harm in trying to recreate existing. + $queue->createQueue(); + $count = $queue->numberOfItems(); + drupal_set_message($this->t('Ran cron. If claimed items expired, they should be expired now. There are now @count items in the queue', array('@count' => $count))); + $form_state->setRebuild(); + } + + /** + * Submit handler for clearing/deleting the queue. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function submitDeleteQueue(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + $queue->deleteQueue(); + drupal_set_message($this->t('Deleted the @queue_name queue and all items in it', array('@queue_name' => $form_state->getValue('queue_name')))); + } + + /** + * Helper method to format a queue item for display in a summary table. + * + * @param array $item + * Queue item array with keys for item_id, expire, created, and data. + * + * @return array + * An array with the queue properties in the right order for display in a + * summary table. + */ + private function processQueueItemForTable(array $item) { + if ($item['expire'] > 0) { + $item['expire'] = $this->t('Claimed: expires %expire', array('%expire' => date('r', $item['expire']))); + } + else { + $item['expire'] = $this->t('Unclaimed'); + } + $item['created'] = date('r', $item['created']); + $item['content'] = Html::escape(unserialize($item['data'])); + unset($item['data']); + + return $item; + } + +} diff --git a/queue_example/src/Tests/QueueExampleTest.php b/queue_example/src/Tests/QueueExampleTest.php new file mode 100644 index 0000000..626bf94 --- /dev/null +++ b/queue_example/src/Tests/QueueExampleTest.php @@ -0,0 +1,80 @@ + 'Queue Example functionality', + 'description' => 'Test Queue Example functionality', + 'group' => 'Examples', + ); + } + + /** + * Login user, create an example node, and test blog functionality through the + * admin and user interfaces. + */ + public function testQueueExampleBasic() { + $this->drupalGet('examples/queue_example'); + // Load the queue with 5 items. + for ($i = 1; $i <= 5; $i++) { + $edit = array('queue_name' => 'queue_example_first_queue', 'string_to_add' => 'boogie' . $i); + $this->drupalPostForm(NULL, $edit, t('Insert into queue')); + $this->assertText(t('There are now @number items in the queue', array('@number' => $i))); + } + // Claim each of the 5 items with a claim time of 0 seconds. + for ($i = 1; $i <= 5; $i++) { + $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0); + $this->drupalPostForm(NULL, $edit, t('Claim the next item from the queue')); + $this->assertPattern(t('%Claimed item id=.*string=@string for 0 seconds.%', array('@string' => 'boogie' . $i))); + } + $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0); + $this->drupalPostForm(NULL, $edit, t('Claim the next item from the queue')); + $this->assertText(t('There were no items in the queue available to claim')); + + // Sleep a second so we can make sure that the timeouts actually time out. + // Local systems work fine with this but apparently the PIFR server is so + // fast that it needs a sleep before the cron run. + sleep(1); + + // Run cron to release expired items. + $this->drupalPostForm(NULL, array(), t('Run cron manually to expire claims')); + + // Claim and delete each of the 5 items which should now be available. + for ($i = 1; $i <= 5; $i++) { + $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0); + $this->drupalPostForm(NULL, $edit, t('Claim the next item and delete it')); + $this->assertPattern(t('%Claimed and deleted item id=.*string=@string for 0 seconds.%', array('@string' => 'boogie' . $i))); + } + // Verify that nothing is left to claim. + $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0); + $this->drupalPostForm(NULL, $edit, t('Claim the next item from the queue')); + $this->assertText(t('There were no items in the queue available to claim')); + } + +}