From faff5edf8d938a0eee51a3b29c26c593fce6a11e Mon Sep 17 00:00:00 2001 From: Aaron Bauman <aaron@messageagency.com> Date: Mon, 3 Apr 2017 17:21:52 -0400 Subject: [PATCH] end of day commit --- config/install/salesforce.settings.yml | 2 + .../schema/salesforce_mapping.schema.yml | 12 + .../src/Entity/MappedObjectInterface.php | 2 +- .../src/Entity/SalesforceMapping.php | 132 +++++++++- .../src/Entity/SalesforceMappingInterface.php | 35 ++- .../Form/SalesforceMappingFormCrudBase.php | 53 +++- .../tests/src/Unit/SalesforceMappingTest.php | 26 +- .../salesforce_pull/salesforce_pull.install | 31 ++- modules/salesforce_pull/src/DeleteHandler.php | 9 +- modules/salesforce_pull/src/QueueHandler.php | 12 +- .../tests/src/Unit/DeleteHandlerTest.php | 4 +- .../tests/src/Unit/QueueHandlerTest.php | 17 +- .../salesforce_push/salesforce_push.install | 22 +- .../salesforce_push.services.yml | 2 +- .../SalesforcePushQueueProcessor/Rest.php | 39 ++- modules/salesforce_push/src/PushQueue.php | 67 ++--- .../src/PushQueueInterface.php | 50 ++++ .../tests/src/Unit/PushQueueTest.php | 53 ++++ .../SalesforcePushQueueProcessorRestTest.php | 232 ++++++++++++++++++ salesforce.routing.yml | 9 + src/Form/AuthorizeForm.php | 4 - src/Form/SettingsForm.php | 159 ++++++++++++ 22 files changed, 876 insertions(+), 96 deletions(-) create mode 100644 modules/salesforce_push/src/PushQueueInterface.php create mode 100644 modules/salesforce_push/tests/src/Unit/PushQueueTest.php create mode 100644 modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php create mode 100644 src/Form/SettingsForm.php diff --git a/config/install/salesforce.settings.yml b/config/install/salesforce.settings.yml index 2abbdb41..cc605518 100644 --- a/config/install/salesforce.settings.yml +++ b/config/install/salesforce.settings.yml @@ -3,3 +3,5 @@ rest_api_version: url: "" version: "39.0" use_latest: true +global_push_limit: 100000 +pull_max_queue_size: 100000 diff --git a/modules/salesforce_mapping/config/schema/salesforce_mapping.schema.yml b/modules/salesforce_mapping/config/schema/salesforce_mapping.schema.yml index e5b63e07..b482677a 100644 --- a/modules/salesforce_mapping/config/schema/salesforce_mapping.schema.yml +++ b/modules/salesforce_mapping/config/schema/salesforce_mapping.schema.yml @@ -31,6 +31,18 @@ salesforce_mapping.salesforce_mapping.*: pull_trigger_date: type: string label: 'Pull Trigger Date Field' + pull_frequency: + type: integer + label: 'Pull Frequency' + push_frequency: + type: integer + label: 'Push Frequency' + push_limit: + type: integer + label: 'Push Time Limit' + push_retries: + type: integer + label: 'Push Retries' sync_triggers: type: mapping label: 'Sync triggers' diff --git a/modules/salesforce_mapping/src/Entity/MappedObjectInterface.php b/modules/salesforce_mapping/src/Entity/MappedObjectInterface.php index 067865d2..7a784b28 100644 --- a/modules/salesforce_mapping/src/Entity/MappedObjectInterface.php +++ b/modules/salesforce_mapping/src/Entity/MappedObjectInterface.php @@ -10,7 +10,7 @@ use Drupal\salesforce\SObject; /** * */ -interface MappedObjectInterface extends EntityChangedInterface, RevisionLogInterface { +interface MappedObjectInterface extends EntityChangedInterface, RevisionLogInterface, EntityInterface { /** * @return return SalesforceMappingInterface diff --git a/modules/salesforce_mapping/src/Entity/SalesforceMapping.php b/modules/salesforce_mapping/src/Entity/SalesforceMapping.php index 8a206d7c..da3486db 100644 --- a/modules/salesforce_mapping/src/Entity/SalesforceMapping.php +++ b/modules/salesforce_mapping/src/Entity/SalesforceMapping.php @@ -4,9 +4,10 @@ namespace Drupal\salesforce_mapping\Entity; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityInterface; -use Drupal\salesforce_mapping\MappingConstants; -use Drupal\salesforce\SelectQuery; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\salesforce\Exception; +use Drupal\salesforce\SelectQuery; +use Drupal\salesforce_mapping\MappingConstants; /** * Defines a Salesforce Mapping configuration entity class. @@ -52,7 +53,11 @@ use Drupal\salesforce\Exception; * "salesforce_object_type", * "drupal_entity_type", * "drupal_bundle", - * "field_mappings" + * "field_mappings", + * "push_limit", + * "push_retries", + * "push_frequency", + * "pull_frequency", * }, * lookup_keys = { * "drupal_entity_type", @@ -165,6 +170,71 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt protected $field_mappings = []; protected $sync_triggers = []; + /** + * Stateful push data for this mapping + * + * @var array + */ + protected $push_info; + + /** + * Statefull pull data for this mapping. + * + * @var array + */ + protected $pull_info; + + /** + * How often (in seconds) to push with this mapping + * + * @var int + */ + protected $push_frequency = 0; + + /** + * Maxmimum number of records to push during a batch. + * + * @var int + */ + protected $push_limit = 0; + + /** + * Maximum number of attempts to push a record before it's considered failed. + * + * @var string + */ + protected $push_retries = 3; + + /** + * How often (in seconds) to pull with this mapping. + * + * @var int + */ + protected $pull_frequency = 0; + + /** + * {@inheritdoc} + */ + public function __construct(array $values, $entity_type) { + parent::__construct($values, $entity_type); + $push_info = $this->state()->get('salesforce.mapping_push_info', []); + if (empty($push_info[$this->id()])) { + $push_info[$this->id()] = [ + 'last_timestamp' => 0, + ]; + } + $this->push_info = $push_info[$this->id()]; + + $pull_info = $this->state()->get('salesforce.sobject_pull_info', []); + if (empty($pull_info[$this->getSalesforceObjectType()])) { + $pull_info[$this->getSalesforceObjectType()] = [ + 'last_pull_timestamp' => 0, + 'last_delete_timestamp' => 0, + ]; + } + $this->pull_info = $pull_info[$this->getSalesforceObjectType()]; + } + /** * {@inheritdoc} */ @@ -186,6 +256,23 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt return parent::save(); } + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + // Update shared pull values across other mappings to same object type. + $pull_mappings = $storage->loadByProperties([ + 'salesforce_object_type' => $this->salesforce_object_type, + ]); + unset($pull_mappings[$this->id()]); + foreach ($pull_mappings as $mapping) { + if ($this->pull_frequency != $mapping->pull_frequency) { + $mapping->pull_frequency = $this->pull_frequency; + $mapping->save(); + } + } + } + /** * {@inheritdoc} */ @@ -341,16 +428,43 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt /** * {@inheritdoc} */ - public function getLastSyncTime() { - return $this->state()->get('salesforce_pull_last_sync_' . $this->getSalesforceObjectType(), NULL); + public function getLastPullTime() { + return $this->pull_info['last_pull_timestamp']; + } + + /** + * {@inheritdoc} + */ + public function setLastPullTime($time) { + return $this->setPullInfo('last_pull_timestamp', $time); + } + + /** + * {@inheritdoc} + */ + public function getNextPullTime() { + return $this->pull_info['last_pull_timestamp'] + $this->pull_frequency; + } + + /** + * {@inheritdoc} + */ + public function getLastPushTime() { + return $this->push_info['last_timestamp']; + } + + /** + * {@inheritdoc} + */ + public function setLastPushTime($time) { + return $this->setPushInfo('last_timestamp', $time); } /** * {@inheritdoc} */ - public function setLastSyncTime($time) { - $this->state()->set('salesforce_pull_last_sync_' . $this->getSalesforceObjectType(), $time); - return $this; + public function getNextPushTime() { + return $this->push_info['last_timestamp'] + $this->push_frequency; } /** @@ -373,7 +487,7 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt $soql->fields[] = $this->getPullTriggerDate(); // If no lastupdate, get all records, else get records since last pull. - $sf_last_sync = $this->getLastSyncTime(); + $sf_last_sync = $this->getLastPullTime(); if ($sf_last_sync) { $last_sync = gmdate('Y-m-d\TH:i:s\Z', $sf_last_sync); $soql->addCondition($this->getPullTriggerDate(), $last_sync, '>'); diff --git a/modules/salesforce_mapping/src/Entity/SalesforceMappingInterface.php b/modules/salesforce_mapping/src/Entity/SalesforceMappingInterface.php index 715a5895..da8b242b 100644 --- a/modules/salesforce_mapping/src/Entity/SalesforceMappingInterface.php +++ b/modules/salesforce_mapping/src/Entity/SalesforceMappingInterface.php @@ -2,12 +2,13 @@ namespace Drupal\salesforce_mapping\Entity; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\EntityInterface; /** * */ -interface SalesforceMappingInterface { +interface SalesforceMappingInterface extends ConfigEntityInterface { // Placeholder interface. // @TODO figure out what to abstract out of SalesforceMapping @@ -126,7 +127,7 @@ interface SalesforceMappingInterface { * @return mixed * integer timestamp of last sync, or NULL. */ - public function getLastSyncTime(); + public function getLastPullTime(); /** * Set this mapping as having been last pulled at $time. @@ -134,7 +135,14 @@ interface SalesforceMappingInterface { * @param int $time * @return $this */ - public function setLastSyncTime($time); + public function setLastPullTime($time); + + /** + * Get the timestamp when the next pull should be processed for this mapping. + * + * @return int + */ + public function getNextPullTime(); /** * Generate a select query to pull records from Salesforce for this mapping. @@ -146,5 +154,26 @@ interface SalesforceMappingInterface { */ public function getPullQuery(array $mapped_fields = []); + /** + * Returns a timstamp when the push queue was last processed for this mapping. + * + * @return int + */ + public function getLastPushTime(); + + /** + * Set the timestamp when the push queue was last process for this mapping. + * + * @param string $time + * @return $this + */ + public function setLastPushTime($time); + + /** + * Get the timestamp when the next push should be processed for this mapping. + * + * @return int + */ + public function getNextPushTime(); } diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php index 7af5acfe..733bb88e 100644 --- a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php +++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php @@ -6,6 +6,7 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Form\FormStateInterface; use Drupal\salesforce_mapping\MappingConstants; +use Drupal\Core\Url; /** * Salesforce Mapping Form base. @@ -19,16 +20,6 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { */ protected $storageController; - /** - * The mapping field plgin controller. - * Note used currently - * @var [type] - */ - //protected $mappingFieldPluginManager; - - // not currently used (1 use commented out) - //protected $pushPluginManager; - /** * {@inheritdoc} * @@ -37,6 +28,15 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { * into smaller chunks. */ public function buildForm(array $form, FormStateInterface $form_state) { + // Perform our salesforce queries first, so that if we can't connect we don't waste time on the rest of the form. + try { + $object_type_options = $this->get_salesforce_object_type_options(); + } + catch (\Exception $e) { + $href = new Url('salesforce.authorize'); + drupal_set_message($this->t('Error when connecting to Salesforce. Please <a href="@href">check your credentials</a> and try again: %message', ['@href' => $href->toString(), '%message' => $e->getMessage()]), 'error'); + return $form; + } $form = parent::buildForm($form, $form_state); $mapping = $this->entity; @@ -134,7 +134,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { '#type' => 'select', '#description' => $this->t('Select a Salesforce object to map.'), '#default_value' => $salesforce_object_type, - '#options' => $this->get_salesforce_object_type_options(), + '#options' => $object_type_options, '#required' => TRUE, '#empty_option' => $this->t('- Select -'), ]; @@ -188,6 +188,37 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { '#default_value' => $mapping->async, ]; + $form['queue']['push_frequency'] = [ + '#title' => t('Push Frequency'), + '#type' => 'number', + '#default_value' => $mapping->push_frequency, + '#description' => t('Enter a frequency, in seconds, for how often this mapping should be used to push data to Salesforce. Enter 0 to push as often as possible. FYI: 1 hour = 3600; 1 day = 86400.'), + '#min' => 0, + ]; + + $form['queue']['push_limit'] = [ + '#title' => t('Push Limit'), + '#type' => 'number', + '#default_value' => $mapping->push_limit, + '#description' => t('Enter the maximum number of records to be pushed to Salesforce during a single queue batch. Enter 0 to process as many records as possible, subject to the global push queue limit.'), + '#min' => 0, + ]; + + $form['queue']['push_retries'] = [ + '#title' => t('Push Retries'), + '#type' => 'number', + '#default_value' => $mapping->push_retries, + '#description' => t('Enter the maximum number of attempts to push a record to Salesforce before it\'s considered failed. Enter 0 for no limit.'), + '#min' => 0, + ]; + + $form['queue']['pull_frequency'] = [ + '#title' => t('Pull Frequency'), + '#type' => 'number', + '#default_value' => $mapping->pull_frequency, + '#description' => t('Enter a frequency, in seconds, for how often this mapping should be used to pull data to Drupal. Enter 0 to pull as often as possible. FYI: 1 hour = 3600; 1 day = 86400. <em>NOTE: pull frequency is shared per-Salesforce Object. The setting is exposed here for convenience.</em>'), + ]; + $form['queue']['weight'] = [ '#title' => t('Weight'), '#type' => 'select', diff --git a/modules/salesforce_mapping/tests/src/Unit/SalesforceMappingTest.php b/modules/salesforce_mapping/tests/src/Unit/SalesforceMappingTest.php index ad6019f4..9cd0a89a 100644 --- a/modules/salesforce_mapping/tests/src/Unit/SalesforceMappingTest.php +++ b/modules/salesforce_mapping/tests/src/Unit/SalesforceMappingTest.php @@ -1,15 +1,18 @@ <?php namespace Drupal\Tests\salesforce_mapping\Unit; -use Drupal\Tests\UnitTestCase; -use Drupal\salesforce_mapping\SalesforceMappingFieldPluginManager; -use Drupal\salesforce_mapping\Entity\SalesforceMapping; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\State\StateInterface; +use Drupal\Tests\UnitTestCase; +use Drupal\salesforce_mapping\Entity\SalesforceMapping; use Drupal\salesforce_mapping\MappingConstants; use Drupal\salesforce_mapping\Plugin\SalesforceMappingField\Properties; +use Drupal\salesforce_mapping\SalesforceMappingFieldPluginManager; use Prophecy\Argument; + /** * Test Object instantitation * @@ -41,6 +44,9 @@ class SalesforceMappingTest extends UnitTestCase { 'key' => 'Drupal_id__c', 'async' => 1, 'pull_trigger_date' => 'LastModifiedDate', + 'push_limit' => 0, + 'push_frequency' => 0, + 'pull_frequency' => 0, 'sync_triggers' => [ MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE => 1, MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE => 1, @@ -92,6 +98,20 @@ class SalesforceMappingTest extends UnitTestCase { $prophecy->createInstance(Argument::any(), Argument::any())->willReturn($sf_mapping_field); $field_manager = $prophecy->reveal(); + + // mock state + $prophecy = $this->prophesize(StateInterface::CLASS); + $prophecy->get('salesforce.sobject_pull_info', Argument::any())->willReturn([]); + $prophecy->get('salesforce.mapping_push_info', Argument::any())->willReturn([$this->id => [ + 'last_timestamp' => 0, + ]]); + $prophecy->set('salesforce.mapping_push_info', Argument::any())->willReturn(null); + $this->state = $prophecy->reveal(); + + $container = new ContainerBuilder(); + $container->set('state', $this->state); + \Drupal::setContainer($container); + $this->mapping = $this->getMockBuilder(SalesforceMapping::CLASS) ->setMethods(['fieldManager']) ->setConstructorArgs([$this->values, $this->entityTypeId]) diff --git a/modules/salesforce_pull/salesforce_pull.install b/modules/salesforce_pull/salesforce_pull.install index 3083af3f..26915949 100644 --- a/modules/salesforce_pull/salesforce_pull.install +++ b/modules/salesforce_pull/salesforce_pull.install @@ -5,22 +5,45 @@ * Install/uninstall tasks for the Salesforce Pull module. */ +use Drupal\salesforce_pull\QueueHandler; + /** * Implements hook_install(). */ function salesforce_pull_install() { - \Drupal::state()->set('salesforce.pull_max_queue_size', 100000); + \Drupal::state()->set('salesforce.pull_max_queue_size', QueueHandler::PULL_MAX_QUEUE_SIZE); } /** * Implements hook_uninstall(). */ function salesforce_pull_uninstall() { - $delete = ['salesforce.pull_max_queue_size']; + $delete = [ + 'salesforce.pull_max_queue_size', + 'salesforce.sobject_pull_info', + ]; + \Drupal::state()->deleteMultiple($delete); +} + +/** + * Convert per-object pull timestamp key-values into one big array. + */ +function salesforce_pull_update_1() { + $pull_info = []; + $delete = []; $objects = \Drupal::service('salesforce.client')->objects(); foreach ($objects as $type) { - $delete[] = 'salesforce_pull_last_delete_' . $type['name']; - $delete[] = 'salesforce_pull_last_sync_' . $type['name']; + $last_del = \Drupal::state()->get('salesforce_pull_last_delete_' . $type); + $last_pull = \Drupal::state()->get('salesforce_pull_last_sync_' . $type); + $delete[] = 'salesforce_pull_last_delete_' . $type; + $delete[] = 'salesforce_pull_last_sync_' . $type; + if (!empty($last_del)) { + $pull_info[$type]['last_delete_timestamp'] = $last_del; + } + if (!empty($last_pull)) { + $pull_info[$type]['last_pull_timestamp'] = $last_pull; + } } + \Drupal::state()->set('salesforce.sobject_pull_info', $pull_info); \Drupal::state()->deleteMultiple($delete); } diff --git a/modules/salesforce_pull/src/DeleteHandler.php b/modules/salesforce_pull/src/DeleteHandler.php index 9ef44bc6..50ac4156 100644 --- a/modules/salesforce_pull/src/DeleteHandler.php +++ b/modules/salesforce_pull/src/DeleteHandler.php @@ -94,8 +94,12 @@ class DeleteHandler { */ public function processDeletedRecords() { // @TODO Add back in SOAP, and use autoloading techniques + $pull_info = $this->state->get('salesforce.sobject_pull_info', []); foreach (array_reverse($this->mappingStorage->getMappedSobjectTypes()) as $type) { - $last_delete_sync = $this->state->get('salesforce_pull_last_delete_' . $type, strtotime('-29 days')); + $last_delete_sync = !empty($pull_info[$type]['last_delete_timestamp']) + ? $pull_info[$type]['last_delete_timestamp'] + : strtotime('-29 days'); + $now = time(); // getDeleted() restraint: startDate must be at least one minute // greater than endDate. @@ -104,7 +108,8 @@ class DeleteHandler { $now_sf = gmdate('Y-m-d\TH:i:s\Z', $now); $deleted = $this->sfapi->getDeleted($type, $last_delete_sync_sf, $now_sf); $this->handleDeletedRecords($deleted, $type); - $this->state->set('salesforce_pull_last_delete_' . $type, $now); + $pull_info[$type]['last_delete_timestamp'] = $now; + $this->state->set('salesforce.sobject_pull_info', $pull_info); } return TRUE; } diff --git a/modules/salesforce_pull/src/QueueHandler.php b/modules/salesforce_pull/src/QueueHandler.php index 650a4256..c653da45 100644 --- a/modules/salesforce_pull/src/QueueHandler.php +++ b/modules/salesforce_pull/src/QueueHandler.php @@ -25,6 +25,8 @@ use Symfony\Component\HttpFoundation\RequestStack; */ class QueueHandler { + const PULL_MAX_QUEUE_SIZE = 100000; + /** * @var \Drupal\salesforce\Rest\RestClientInterface */ @@ -86,11 +88,11 @@ class QueueHandler { public function getUpdatedRecords() { // Avoid overloading the processing queue and pass this time around if it's // over a configurable limit. - if ($this->queue->numberOfItems() > $this->state->get('salesforce_pull_max_queue_size', 100000)) { + if ($this->queue->numberOfItems() > $this->state->get('salesforce.pull_max_queue_size', self::PULL_MAX_QUEUE_SIZE)) { $message = 'Pull Queue contains %noi items, exceeding the max size of %max items. Pull processing will be blocked until the number of items in the queue is reduced to below the max size.'; $args = [ '%noi' => $this->queue->numberOfItems(), - '%max' => $this->state->get('salesforce_pull_max_queue_size', 100000), + '%max' => $this->state->get('salesforce.pull_max_queue_size', self::PULL_MAX_QUEUE_SIZE), ]; $this->eventDispatcher->dispatch(SalesforceEvents::NOTICE, new SalesforceNoticeEvent(NULL, $message, $args)); return FALSE; @@ -98,12 +100,16 @@ class QueueHandler { // Iterate over each field mapping to determine our query parameters. foreach ($this->mappings as $mapping) { + if ($mapping->getNextPullTime() > $this->request->server->get('REQUEST_TIME')) { + // Skip this mapping, based on pull frequency. + continue; + } $results = $this->doSfoQuery($mapping); if ($results) { $this->enqueueAllResults($mapping, $results); // @TODO Replace this with a better implementation when available, // see https://www.drupal.org/node/2820345, https://www.drupal.org/node/2785211 - $mapping->setLastSyncTime($this->request->server->get('REQUEST_TIME')); + $mapping->setLastPullTime($this->request->server->get('REQUEST_TIME')); } } return TRUE; diff --git a/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php b/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php index eeabd9e2..348e3495 100644 --- a/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php +++ b/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php @@ -132,8 +132,8 @@ class DeleteHandlerTest extends UnitTestCase { // Mock state. $prophecy = $this->prophesize(StateInterface::CLASS); - $prophecy->get('salesforce_pull_last_delete_default', Argument::any())->willReturn('1485787434'); - $prophecy->set('salesforce_pull_last_delete_default', Argument::any())->willReturn(null); + $prophecy->get('salesforce.sobject_pull_info', Argument::any())->willReturn(['default' => ['last_delete_timestamp' => '1485787434']]); + $prophecy->set('salesforce.sobject_pull_info', Argument::any())->willReturn(null); $this->state = $prophecy->reveal(); // mock event dispatcher diff --git a/modules/salesforce_pull/tests/src/Unit/QueueHandlerTest.php b/modules/salesforce_pull/tests/src/Unit/QueueHandlerTest.php index 8f9d8916..b5aa0c76 100644 --- a/modules/salesforce_pull/tests/src/Unit/QueueHandlerTest.php +++ b/modules/salesforce_pull/tests/src/Unit/QueueHandlerTest.php @@ -1,7 +1,7 @@ <?php namespace Drupal\Tests\salesforce_pull\Unit; -use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Queue\QueueDatabaseFactory; use Drupal\Core\Queue\QueueInterface; @@ -60,6 +60,9 @@ class QueueHandlerTest extends UnitTestCase { $this->mapping->expects($this->any()) ->method('getPullFieldsArray') ->willReturn(['Name' => 'Name', 'Account Number' => 'Account Number']); + $this->mapping->expects($this->any()) + ->method('getNextPullTime') + ->willReturn(0); $prophecy = $this->prophesize(QueueInterface::CLASS); $prophecy->createItem()->willReturn(1); @@ -83,13 +86,13 @@ class QueueHandlerTest extends UnitTestCase { // mock state $prophecy = $this->prophesize(StateInterface::CLASS); - $prophecy->get('salesforce_pull_last_sync_default', Argument::any())->willReturn('1485787434'); - $prophecy->get('salesforce_pull_max_queue_size', Argument::any())->willReturn('100000'); - $prophecy->set('salesforce_pull_last_sync_default', Argument::any())->willReturn(null); + $prophecy->get('salesforce.sobject_pull_info', Argument::any())->willReturn(['default' => ['last_pull_timestamp' => '0']]); + $prophecy->get('salesforce.pull_max_queue_size', Argument::any())->willReturn(QueueHandler::PULL_MAX_QUEUE_SIZE); + $prophecy->set('salesforce.sobject_pull_info', Argument::any())->willReturn(null); $this->state = $prophecy->reveal(); // mock event dispatcher - $prophecy = $this->prophesize(ContainerAwareEventDispatcher::CLASS); + $prophecy = $this->prophesize(EventDispatcherInterface::CLASS); $prophecy->dispatch(Argument::any(), Argument::any())->willReturn(); $this->ed = $prophecy->reveal(); @@ -100,10 +103,10 @@ class QueueHandlerTest extends UnitTestCase { // mock request $request = $this->prophesize(Request::CLASS); + $request->server = $this->server; // mock request stack $prophecy = $this->prophesize(RequestStack::CLASS); - $prophecy->server = $this->server; $prophecy->getCurrentRequest()->willReturn($request->reveal()); $this->request_stack = $prophecy->reveal(); @@ -138,7 +141,7 @@ class QueueHandlerTest extends UnitTestCase { // initialize with queue size > 100000 (default) $prophecy = $this->prophesize(QueueInterface::CLASS); $prophecy->createItem()->willReturn(1); - $prophecy->numberOfItems()->willReturn(100001); + $prophecy->numberOfItems()->willReturn(QueueHandler::PULL_MAX_QUEUE_SIZE + 1); $this->queue = $prophecy->reveal(); $prophecy = $this->prophesize(QueueDatabaseFactory::CLASS); diff --git a/modules/salesforce_push/salesforce_push.install b/modules/salesforce_push/salesforce_push.install index e0020db5..8f442a57 100644 --- a/modules/salesforce_push/salesforce_push.install +++ b/modules/salesforce_push/salesforce_push.install @@ -6,7 +6,8 @@ use Drupal\salesforce_push\PushQueue; * Implements hook_install(). */ function salesforce_push_install() { - \Drupal::state()->set('salesforce.push_limit', PushQueue::DEFAULT_CRON_PUSH_LIMIT); + \Drupal::state()->set('salesforce.mapping_push_limit', PushQueue::MAPPING_CRON_PUSH_LIMIT); + \Drupal::state()->set('salesforce.global_push_limit', PushQueue::GLOBAL_CRON_PUSH_LIMIT); \Drupal::state()->set('salesforce.push_queue_processor', PushQueue::DEFAULT_QUEUE_PROCESSOR); \Drupal::state()->set('salesforce.push_queue_max_fails', PushQueue::DEFAULT_MAX_FAILS); } @@ -19,13 +20,28 @@ function salesforce_push_uninstall() { 'salesforce.push_limit', 'salesforce.push_queue_processor', 'salesforce.push_queue_max_fails', + 'salesforce.mapping_push_info', ]; \Drupal::state()->deleteMultiple($delete); \Drupal::service('queue.salesforce_push')->deleteTable(); } +/** + * Set default variables + * + * @return void + * @author Aaron Bauman + */ function salesforce_push_update_1() { - \Drupal::state()->set('salesforce.push_queue_processor', 'rest'); - \Drupal::state()->set('salesforce.push_queue_max_fails', 10); + \Drupal::state()->set('salesforce.push_queue_processor', PushQueue::DEFAULT_QUEUE_PROCESSOR); + \Drupal::state()->set('salesforce.push_queue_max_fails', PushQueue::DEFAULT_MAX_FAILS); } +/** + * Create new variables for more granualar push limits. + */ +function salesforce_push_update_2() { + \Drupal::state()->set('salesforce.global_push_limit', PushQueue::GLOBAL_CRON_PUSH_LIMIT); + + \Drupal::state()->delete('salesforce.push_limit'); +} \ No newline at end of file diff --git a/modules/salesforce_push/salesforce_push.services.yml b/modules/salesforce_push/salesforce_push.services.yml index cd30ad22..193c43a1 100644 --- a/modules/salesforce_push/salesforce_push.services.yml +++ b/modules/salesforce_push/salesforce_push.services.yml @@ -5,4 +5,4 @@ services: queue.salesforce_push: class: Drupal\salesforce_push\PushQueue - arguments: ['@database', '@state', '@plugin.manager.salesforce_push_queue_processor', '@entity.manager', '@event_dispatcher'] + arguments: ['@database', '@state', '@plugin.manager.salesforce_push_queue_processor', '@entity.manager', '@event_dispatcher', '@request_stack'] diff --git a/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php b/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php index 706476f6..f6e2bd5f 100644 --- a/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php +++ b/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php @@ -2,19 +2,20 @@ namespace Drupal\salesforce_push\Plugin\SalesforcePushQueueProcessor; -use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher; -use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Queue\SuspendQueueException; use Drupal\salesforce\EntityNotFoundException; -use Drupal\salesforce\Rest\RestClientInterface; use Drupal\salesforce\Event\SalesforceEvents; +use Drupal\salesforce\Rest\RestClientInterface; use Drupal\salesforce_mapping\Entity\MappedObject; -use Drupal\salesforce_mapping\MappingConstants; +use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface; use Drupal\salesforce_mapping\Event\SalesforcePushOpEvent; -use Drupal\salesforce_push\PushQueue; +use Drupal\salesforce_mapping\MappingConstants; +use Drupal\salesforce_push\PushQueueInterface; use Drupal\salesforce_push\PushQueueProcessorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Rest queue processor plugin. @@ -44,7 +45,7 @@ class Rest extends PluginBase implements PushQueueProcessorInterface { protected $event_dispatcher; protected $etm; - public function __construct(array $configuration, $plugin_id, array $plugin_definition, PushQueue $queue, RestClientInterface $client, EntityTypeManager $etm, ContainerAwareEventDispatcher $event_dispatcher) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, PushQueueInterface $queue, RestClientInterface $client, EntityTypeManagerInterface $etm, EventDispatcherInterface $event_dispatcher) { $this->queue = $queue; $this->client = $client; $this->etm = $etm; @@ -80,7 +81,7 @@ class Rest extends PluginBase implements PushQueueProcessorInterface { } } - protected function processItem(\stdClass $item) { + public function processItem(\stdClass $item) { $mapped_object = $this ->mapped_object_storage ->load($item->mapped_object_id); @@ -93,11 +94,7 @@ class Rest extends PluginBase implements PushQueueProcessorInterface { // If mapped object doesn't exist or fails to load for this delete, this item can be considered successfully processed. return; } - $mapped_object = new MappedObject([ - 'entity_id' => $item->entity_id, - 'entity_type_id' => $mapping->drupal_entity_type, - 'salesforce_mapping' => $mapping->id(), - ]); + $mapped_object = $this->createMappedObject($item, $mapping); } // @TODO: the following is nearly identical to the end of salesforce_push_entity_crud(). Can we DRY it? Do we care? @@ -115,7 +112,7 @@ class Rest extends PluginBase implements PushQueueProcessorInterface { $entity = $this->etm ->getStorage($mapping->drupal_entity_type) ->load($item->entity_id); - if (!$entity) { + if ($entity === NULL) { // Bubble this up also throw new EntityNotFoundException($item->entity_id, $mapping->drupal_entity_type); } @@ -145,4 +142,20 @@ class Rest extends PluginBase implements PushQueueProcessorInterface { } } + /** + * Helper method to generate a new MappedObject during push procesing. + * + * @param string $item + * @param string $mapping + * @return void + * @author Aaron Bauman + */ + protected function createMappedObject(\stdClass $item, SalesforceMappingInterface $mapping) { + return new MappedObject([ + 'entity_id' => $item->entity_id, + 'entity_type_id' => $mapping->drupal_entity_type, + 'salesforce_mapping' => $mapping->id(), + ]); + } + } diff --git a/modules/salesforce_push/src/PushQueue.php b/modules/salesforce_push/src/PushQueue.php index 865526bb..ef6c13c8 100644 --- a/modules/salesforce_push/src/PushQueue.php +++ b/modules/salesforce_push/src/PushQueue.php @@ -15,6 +15,7 @@ use Drupal\salesforce\Event\SalesforceErrorEvent; use Drupal\salesforce\Event\SalesforceNoticeEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Drupal\salesforce\Event\SalesforceEvents; +use Symfony\Component\HttpFoundation\RequestStack; /** * Salesforce push queue. @@ -28,13 +29,16 @@ class PushQueue extends DatabaseQueue { */ const TABLE_NAME = 'salesforce_push_queue'; - const DEFAULT_CRON_PUSH_LIMIT = 200; + const GLOBAL_CRON_PUSH_LIMIT = 10000; const DEFAULT_QUEUE_PROCESSOR = 'rest'; const DEFAULT_MAX_FAILS = 10; - protected $limit; + const DEFAULT_LEASE_TIME = 300; + + protected $mapping_limit; + protected $global_limit; protected $connection; protected $state; protected $logger; @@ -56,13 +60,18 @@ class PushQueue extends DatabaseQueue { */ protected $mapped_object_storage; + /** + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + /** * Constructs a \Drupal\Core\Queue\DatabaseQueue object. * * @param \Drupal\Core\Database\Connection $connection * The Connection object containing the key-value tables. */ - public function __construct(Connection $connection, State $state, PushQueueProcessorPluginManager $queue_manager, EntityManagerInterface $entity_manager, EventDispatcherInterface $event_dispatcher) { + public function __construct(Connection $connection, State $state, PushQueueProcessorPluginManager $queue_manager, EntityManagerInterface $entity_manager, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack) { $this->connection = $connection; $this->state = $state; $this->queueManager = $queue_manager; @@ -70,9 +79,9 @@ class PushQueue extends DatabaseQueue { $this->mapping_storage = $entity_manager->getStorage('salesforce_mapping'); $this->mapped_object_storage = $entity_manager->getStorage('salesforce_mapped_object'); $this->eventDispatcher = $event_dispatcher; + $this->request = $request_stack->getCurrentRequest(); - $this->limit = $state->get('salesforce.push_limit', static::DEFAULT_CRON_PUSH_LIMIT); - + $this->global_limit = $state->get('salesforce.global_push_limit', static::GLOBAL_CRON_PUSH_LIMIT); $this->max_fails = $state->get('salesforce.push_queue_max_fails', static::DEFAULT_MAX_FAILS); } @@ -115,7 +124,7 @@ class PushQueue extends DatabaseQueue { throw new \Exception('Salesforce push queue data values are required for "name", "entity_id" and "op"'); } $this->name = $data['name']; - $time = time(); + $time = $this->request->server->get('REQUEST_TIME'); $fields = [ 'name' => $this->name, 'entity_id' => $data['entity_id'], @@ -148,21 +157,14 @@ class PushQueue extends DatabaseQueue { } /** - * Claim up to $n items from the current queue. - * - * If queue is empty, return an empty array. - * - * @see DatabaseQueue::claimItem - * - * @return array - * Zero to $n Items indexed by item_id + * {@inheritdoc} */ - public function claimItems($n, $lease_time = 300) { + public function claimItems($n, $fail_limit = self::DEFAULT_MAX_FAILS, $lease_time = self::DEFAULT_LEASE_TIME) { while (TRUE) { try { // @TODO: convert items to content entities. // @see \Drupal::entityQuery() - $items = $this->connection->queryRange('SELECT * FROM {' . static::TABLE_NAME . '} q WHERE expire = 0 AND name = :name AND failures < :fail_limit ORDER BY created, item_id ASC', 0, $n, array(':name' => $this->name, ':fail_limit' => $this->max_fails))->fetchAllAssoc('item_id'); + $items = $this->connection->queryRange('SELECT * FROM {' . static::TABLE_NAME . '} q WHERE expire = 0 AND name = :name AND failures < :fail_limit ORDER BY created, item_id ASC', 0, $n, array(':name' => $this->name, ':fail_limit' => $fail_limit))->fetchAllAssoc('item_id'); } catch (\Exception $e) { $this->catchException($e); @@ -179,7 +181,7 @@ class PushQueue extends DatabaseQueue { // should really expire. $update = $this->connection->update(static::TABLE_NAME) ->fields(array( - 'expire' => time() + $lease_time, + 'expire' => $this->request->server->get('REQUEST_TIME') + $lease_time, )) ->condition('item_id', array_keys($items), 'IN') ->condition('expire', 0); @@ -196,9 +198,7 @@ class PushQueue extends DatabaseQueue { } /** - * DO NOT USE THIS FUNCTION. - * - * Use claimItems() instead. + * {@inheritdoc} */ public function claimItem($lease_time = NULL) { throw new \Exception('This queue is designed to process multiple items at once. Please use "claimItems" instead.'); @@ -298,14 +298,23 @@ class PushQueue extends DatabaseQueue { $queue_processor = $this->queueManager->createInstance($plugin_name); foreach ($mappings as $mapping) { + $j = 0; + // Check mapping frequency before proceeding. + if ($mapping->getNextPushTime() > $this->request->server->get('REQUEST_TIME')) { + continue; + } + // Set the queue name, which is the mapping id. $this->setName($mapping->id()); - // Iterate through items in this queue until we run out or hit the limit. + // Iterate through items in this queue (mapping) until we run out or hit + // the mapping limit, then move to the next queue. If we hit the global + // limit, return immediately. while (TRUE) { // Claim as many items as we can from this queue and advance our counter. If this queue is empty, move to the next mapping. - $items = $this->claimItems($this->limit); + $items = $this->claimItems($mapping->push_limit, $mapping->push_retries); if (empty($items)) { + $mapping->setLastPushTime($this->request->server->get('REQUEST_TIME')); continue 2; } @@ -336,23 +345,21 @@ class PushQueue extends DatabaseQueue { finally { // If we've reached our limit, we're done. Otherwise, continue to next items. $i += count($items); - if ($i >= $this->limit) { + $j += count($items); + if ($i >= $this->global_limit) { return $this; } } + if ($mapping_limit && $j > $mapping_limit) { + continue 2; + } } } return $this; } /** - * Failed item handler. - * - * Exception handler so that Queue Processors don't have to worry about what - * happens when a queue item fails. - * - * @param Exception $e - * @param stdClass $item + * {@inheritdoc} */ public function failItem(\Exception $e, \stdClass $item) { $mapping = $this->mapping_storage->load($item->name); diff --git a/modules/salesforce_push/src/PushQueueInterface.php b/modules/salesforce_push/src/PushQueueInterface.php new file mode 100644 index 00000000..712636ac --- /dev/null +++ b/modules/salesforce_push/src/PushQueueInterface.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\salesforce_push; + +use Drupal\Core\Queue\ReliableQueueInterface; + +interface PushQueueInterface extends ReliableQueueInterface { + + /** + * Claim up to $n items from the current queue. + * + * If queue is empty, return an empty array. + * + * @see DatabaseQueue::claimItem + * + * @param int $n + * Number of items to claim. + * @param int $fail_limit + * Do not claim items with this many or more failures. + * @param int $lease_time + * Time, in seconds, for which to hold this claim. + * + * @return array + * Zero to $n Items indexed by item_id + */ + public function claimItems($n, $fail_limit = 0, $lease_time = 0); + + /** + * Inherited classes must throw an exception when this method is called. + * Use claimItems() instead. + * + * @throws \Exception + * Whenever called. + */ + public function claimItem($lease_time = NULL); + + /** + * Failed item handler. + * + * Exception handler so that Queue Processors don't have to worry about what + * happens when a queue item fails. + * + * @param Exception $e + * @param stdClass $item + */ + public function failItem(\Exception $e, \stdClass $item); + + +} + diff --git a/modules/salesforce_push/tests/src/Unit/PushQueueTest.php b/modules/salesforce_push/tests/src/Unit/PushQueueTest.php new file mode 100644 index 00000000..ccfd115b --- /dev/null +++ b/modules/salesforce_push/tests/src/Unit/PushQueueTest.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\Tests\salesforce_push\Unit; + +use Drupal\salesforce_push\PushQueue; +use Drupal\Tests\UnitTestCase; + +/** + * Test Object instantitation + * + * @coversDefaultClass \Drupal\salesforce_push\PushQueue + * + * @group salesforce_push + */ + +class PushQueueTest extends UnitTestCase { + static $modules = ['salesforce_push']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + } + + /** + * @covers ::claimItems + */ + public function testClaimItems() { + // Test basic claim items + // Test claim items with different mappings + // Test claim items excluding failed items + } + + /** + * @covers ::processQueues + */ + public function testProcessQueues() { + // Test creating a queue processor + // Test mapping frequency + // Test mapping push limit + // Test global push limit + // Test queue processor throwing RequeueException + // Test queue processor throwing SuspendQueueException + } + + /** + * @covers ::failItem + */ + public function testFailItem() { + // Test failed item gets its "fail" property incremented by 1 + } + +} diff --git a/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php b/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php new file mode 100644 index 00000000..99b29ff3 --- /dev/null +++ b/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php @@ -0,0 +1,232 @@ +<?php + +namespace Drupal\Tests\salesforce_push\Unit; + +use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use Drupal\Tests\UnitTestCase; +use Drupal\salesforce\Rest\RestClientInterface; +use Drupal\salesforce_mapping\Entity\MappedObjectInterface; +use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface; +use Drupal\salesforce_mapping\MappingConstants; +use Drupal\salesforce_push\Plugin\SalesforcePushQueueProcessor\Rest; +use Drupal\salesforce_push\PushQueueInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\Entity\EntityManagerInterface; + +/** + * Test SalesforcePushQueueProcessor plugin Rest + * + * @coversDefaultClass \Drupal\salesforce_push\Plugin\SalesforcePushQueueProcessor\Rest + * + * @group salesforce_pull + */ + +class SalesforcePushQueueProcessorRestTest extends UnitTestCase { + static $modules = ['salesforce_pull']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->entityType = 'default'; + + $this->queue = $this->getMock(PushQueueInterface::CLASS); + $this->client = $this->getMock(RestClientInterface::CLASS); + $this->entityTypeManager = $this->getMock(EntityTypeManagerInterface::CLASS); + $this->eventDispatcher = $this->getMock(EventDispatcherInterface::CLASS); + $this->eventDispatcher->expects($this->any()) + ->method('dispatch') + ->willReturn(NULL); + + $this->string_translation = $this->getMock(TranslationInterface::class); + $this->entity_manager = $this->getMock(EntityManagerInterface::class); + + $container = new ContainerBuilder(); + $container->set('queue.salesforce_push', $this->queue); + $container->set('salesforce.client', $this->client); + $container->set('entity_type.manager', $this->entityTypeManager); + $container->set('event_dispatcher', $this->eventDispatcher); + $container->set('string_translation', $this->string_translation); + $container->set('entity.manager', $this->entity_manager); + \Drupal::setContainer($container); + + $this->mapping = $this->getMock(SalesforceMappingInterface::CLASS); + + $this->mapping->expects($this->any()) + ->method('__get') + ->with($this->equalTo('drupal_entity_type')) + ->willReturn($this->entityType); + + $this->mapping_storage = $this->getMock(ConfigEntityStorageInterface::CLASS); + + $this->mapped_object_storage = $this->getMock(SqlEntityStorageInterface::CLASS); + + $this->entityStorage = $this->getMock(SqlEntityStorageInterface::CLASS); + + $this->entityTypeManager->expects($this->at(0)) + ->method('getStorage') + ->with($this->equalTo('salesforce_mapping')) + ->willReturn($this->mapping_storage); + + $this->entityTypeManager->expects($this->at(1)) + ->method('getStorage') + ->with($this->equalTo('salesforce_mapped_object')) + ->willReturn($this->mapped_object_storage); + + $this->mapping_storage->expects($this->any()) + ->method('load') + ->willReturn($this->mapping); + + $this->handler = new Rest([], '', [], $this->queue, $this->client, $this->entityTypeManager, $this->eventDispatcher); + + } + + /** + * @covers ::process + * @expectedException \Drupal\Core\Queue\SuspendQueueException + */ + public function testProcess() { + $this->handler = $this->getMock(Rest::class, ['processItem'], [[], '', [], $this->queue, $this->client, $this->entityTypeManager, $this->eventDispatcher]); + + $this->client->expects($this->at(0)) + ->method('isAuthorized') + ->willReturn(TRUE); + + // test suspend queue if not authorized + $this->client->expects($this->at(1)) + ->method('isAuthorized') + ->willReturn(FALSE); + + $this->handler->expects($this->once()) + ->method('processItem') + ->willReturn(NULL); + + // test delete item after successful processItem() + $this->queue->expects($this->once()) + ->method('deleteItem') + ->willReturn(NULL); + + $this->handler->process([(object)[1]]); + $this->handler->process([(object)[2]]); + } + + /** + * @covers ::processItem + */ + public function testProcessItemDeleteNoop() { + // test noop for op == delete and no mapped object + $this->mapped_object_storage->expects($this->once()) + ->method('load') + ->willReturn(FALSE); + + $this->handler->processItem((object)['op' => MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE, 'mapped_object_id' => 'foo', 'name' => 'bar']); + } + + /** + * @covers ::processItem + */ + public function testProcessItemDelete() { + // test push delete for op == delete + $this->mappedObject = $this->getMock(MappedObjectInterface::class); + $this->queueItem = (object)[ + 'op' => MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE, + 'mapped_object_id' => 'foo', + 'name' => 'bar' + ]; + + $this->mapped_object_storage->expects($this->once()) + ->method('load') + ->willReturn($this->mappedObject); + + $this->mappedObject->expects($this->once()) + ->method('pushDelete') + ->willReturn(NULL); + + // test skip item on missing mapped object and op == delete + // test push on op == insert / update + // test throwing exception on drupal entity not found + + $this->handler->processItem($this->queueItem); + } + + /** + * @covers ::processItem + */ + public function testProcessItemPush() { + // test push on op == insert / update + $this->mappedObject = $this->getMock(MappedObjectInterface::class); + $this->queueItem = (object)[ + 'entity_id' => 'foo', + 'op' => NULL, + 'mapped_object_id' => NULL, + 'name' => NULL, + ]; + $this->entity = $this->getMock(EntityInterface::class); + + $this->entityTypeManager->expects($this->once()) + ->method('getStorage') + ->with($this->equalTo($this->entityType)) + ->willReturn($this->entityStorage); + + $this->entityStorage->expects($this->once()) + ->method('load') + ->willReturn($this->entity); + + $this->mapped_object_storage->expects($this->once()) + ->method('load') + ->willReturn($this->mappedObject); + + $this->mappedObject->expects($this->once()) + ->method('setDrupalEntity') + ->willReturn($this->mappedObject); + + $this->mappedObject->expects($this->once()) + ->method('push') + ->willReturn(NULL); + + $this->handler->processItem($this->queueItem); + + } + + /** + * @covers ::processItem + * + * @expectedException \Drupal\salesforce\EntityNotFoundException + */ + public function testProcessItemEntityNotFound() { + // test throwing exception on drupal entity not found + $this->queueItem = (object)[ + 'op' => '', + 'mapped_object_id' => 'foo', + 'name' => 'bar', + 'entity_id' => 'foo', + ]; + + $this->mappedObject = $this->getMock(MappedObjectInterface::class); + $this->mappedObject->expects($this->once()) + ->method('isNew') + ->willReturn(TRUE); + + $this->mapped_object_storage->expects($this->once()) + ->method('load') + ->willReturn($this->mappedObject); + + $this->entityTypeManager->expects($this->once()) + ->method('getStorage') + ->with($this->equalTo($this->entityType)) + ->willReturn($this->entityStorage); + + $this->entityStorage->expects($this->once()) + ->method('load') + ->willReturn(NULL); + + $this->handler->processItem($this->queueItem); + } + +} + diff --git a/salesforce.routing.yml b/salesforce.routing.yml index c6d8df11..c1f88695 100644 --- a/salesforce.routing.yml +++ b/salesforce.routing.yml @@ -14,6 +14,15 @@ salesforce.authorize: requirements: _permission: 'authorize salesforce' +salesforce.global_settings: + path: '/admin/config/salesforce/settings' + defaults: + _controller: '\Drupal\salesforce\Form\SettingsForm' + _title: 'Salesforce' + _description: 'Manage global settings for Salesforce Suite.' + requirements: + _permission: 'administer salesforce' + salesforce.admin_config_salesforce: path: '/admin/config/salesforce' defaults: diff --git a/src/Form/AuthorizeForm.php b/src/Form/AuthorizeForm.php index 0ceaaf60..c9d7868a 100644 --- a/src/Form/AuthorizeForm.php +++ b/src/Form/AuthorizeForm.php @@ -40,8 +40,6 @@ class AuthorizeForm extends ConfigFormBase { */ protected $state; - protected $logger; - /** * Constructs a \Drupal\system\ConfigFormBase object. * @@ -51,8 +49,6 @@ class AuthorizeForm extends ConfigFormBase { * The factory for configuration objects. * @param \Drupal\Core\State\StateInterface $state * The state keyvalue collection to use. - * @param \Drupal\Core\Logger\LoggerChannelFactory $logger_factory - * The logger factory service. */ public function __construct(ConfigFactoryInterface $config_factory, RestClientInterface $salesforce_client, StateInterface $state, EventDispatcherInterface $event_dispatcher) { parent::__construct($config_factory); diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php new file mode 100644 index 00000000..9383ed7d --- /dev/null +++ b/src/Form/SettingsForm.php @@ -0,0 +1,159 @@ +<?php + +namespace Drupal\salesforce\Form; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\TrustedRedirectResponse; +use Drupal\Core\State\StateInterface; +use Drupal\salesforce\Rest\RestClientInterface; +use GuzzleHttp\Exception\RequestException; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Drupal\salesforce\Event\SalesforceEvents; +use Drupal\salesforce\Event\SalesforceErrorEvent; + +/** + * Creates authorization form for Salesforce. + */ +class AuthorizeForm extends ConfigFormBase { + + /** + * The Salesforce REST client. + * + * @var \Drupal\salesforce\Rest\RestClientInterface + */ + protected $sf_client; + + /** + * The sevent dispatcher service.. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $eventDispatcher; + + /** + * The state keyvalue collection. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * Constructs a \Drupal\system\ConfigFormBase object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\salesforce\RestClient $salesforce_client + * The factory for configuration objects. + * @param \Drupal\Core\State\StateInterface $state + * The state keyvalue collection to use. + */ + public function __construct(ConfigFactoryInterface $config_factory, RestClientInterface $salesforce_client, StateInterface $state, EventDispatcherInterface $event_dispatcher) { + parent::__construct($config_factory); + $this->sf_client = $salesforce_client; + $this->state = $state; + $this->eventDispatcher = $event_dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('salesforce.client'), + $container->get('state'), + $container->get('event_dispatcher') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'salesforce_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return [ + 'salesforce.settings', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // We're not actually doing anything with this, but may figure out + // something that makes sense. + $config = $this->config('salesforce.settings'); + + $form['use_latest'] = [ + '#title' => $this->t('Use Latest Rest API version (recommended)'), + '#type' => 'checkbox', + '#description' => $this->t('Always use the latest Rest API version when connecting to Salesforce. In general, Rest API is backwards-compatible for many years. Unless you have a very specific reason, you should probably just use the latest version.'), + ]; + $versions = []; + $form['rest_api_version']['version'] = [ + '#title' => $this->t('Select a specific Rest API version (advanced)'), + '#type' => 'select', + '#options' => $versions, + '#tree' => TRUE, + ]; + + $form['push_limit'] = [ + '#title' => $this->t('Global push limit'), + '#type' => 'number', + '#description' => $this->t('Set the maximum number of records to be processed during each push queue process. Enter 0 for no limit.'), + '#required' => TRUE, + '#min' => 0, + ]; + + $form['pull_max_queue_size'] = [ + '#title' => $this->t('Pull queue max size'), + '#type' => 'password', + '#description' => $this->t('Set the maximum number of items which can be enqueued for pull at any given time. Note this setting is not exactly analogous to the push queue limit, since Drupal Cron API does not offer such granularity. Enter 0 for no limit.'), + '#required' => TRUE, + '#min' => 0, + ]; + + $form = parent::buildForm($form, $form_state); + $form['creds']['actions'] = $form['actions']; + unset($form['actions']); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + $this->sf_client->setConsumerKey($values['consumer_key']); + $this->sf_client->setConsumerSecret($values['consumer_secret']); + $this->sf_client->setLoginUrl($values['login_url']); + + try { + $path = $this->sf_client->getAuthEndpointUrl(); + $query = [ + 'redirect_uri' => $this->sf_client->getAuthCallbackUrl(), + 'response_type' => 'code', + 'client_id' => $values['consumer_key'], + ]; + + // Send the user along to the Salesforce OAuth login form. If successful, + // the user will be redirected to {redirect_uri} to complete the OAuth + // handshake. + $form_state->setResponse(new TrustedRedirectResponse($path . '?' . http_build_query($query), 302)); + } + catch (RequestException $e) { + drupal_set_message(t("Error during authorization: %message", ['%message' => $e->getMessage()]), 'error'); + $this->eventDispatcher->dispatch(SalesforceEvents::ERROR, new SalesforceErrorEvent($e)); + } + } + +} -- GitLab