diff --git a/config/install/salesforce.settings.yml b/config/install/salesforce.settings.yml index 2abbdb41498ea113f2c3316a78f81ff5a1ddd418..cc605518f110e02c52c8509e2c76371a68fe2895 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 e5b63e0716cd3f9be18c2b84908b623cfa32d3b1..b482677a9eefe77b7410518112e6b3acdcee5eb5 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 067865d2bc07b6675864bef2e87837e21eb11d5c..7a784b2895d3aefee986dee8593062bf4fb1a26d 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 8a206d7c45b9d2a47f768818de0e3be72c626d42..da3486db983dcdcb01ce681ff58eb69ea105e043 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 715a5895f43b9a9ded2ee4862838a3674d85acfb..da8b242b920297aba5075576e9ea1316502090f5 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 7af5acfe380449eebb40d387feec55c15606e4b1..733bb88e2bc0f28dcdb29645228e047637cc79c6 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 ad6019f4ee0125c5a80b0de99423a2e431c32914..9cd0a89a930118565e238afec230ebbc078deb03 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 3083af3f575295d937519f8a4a4bbd02434c4914..269159496af7892635bacd7da3fc36be19b78081 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 9ef44bc6ea9d01c167f00e576b25721162d10a0b..50ac41560b42012df0f62956652df798b8c00102 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 650a42561146546f41b237fdf8eaf9ed2d5da51b..c653da45fc9a54fd7c77973be80101fd80d1c9ad 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 eeabd9e21f424972261a95e7ce00469c749f84a6..348e349569292ebb989b0f743b83422607df0b9c 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 8f9d8916c6042df942ca7ade3be86252aab18806..b5aa0c76cc9bb1a7c557cbba7e592b4204de4efa 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 e0020db50f610adcf5728f32167beefadaa3fae9..8f442a57fca1350c8aadc67a509db92e282dba55 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 cd30ad2239c237ba5d7d12525de1e77aeca8b290..193c43a12ebbd8f34166a0c7b4d73bce3b7aa807 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 706476f6455c18d237a20cf6345f2a39b3badf91..f6e2bd5f3245300504cafa41b6f38c51f948b62b 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 865526bb2d715caaf2d6d27b3ebbc92d67e88321..ef6c13c89a3e7c9543b24c710d3ee223a5cb2d28 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 0000000000000000000000000000000000000000..712636ac115a4e438f42975804407ca2e632d58b --- /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 0000000000000000000000000000000000000000..ccfd115b8fa3449024a66d736700123e703fb86c --- /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 0000000000000000000000000000000000000000..99b29ff31bf35c5300087877d5a6cbac1b4c905a --- /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 c6d8df1103a28240783da49af5b2ae19a359ecdb..c1f88695d14b7b5baf3beae71d6775fca2e48e03 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 0ceaaf6031122c2554fc980fa8c995d3be255f2d..c9d7868a28bb50779ea9de9711216e2931dcd556 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 0000000000000000000000000000000000000000..9383ed7d3519778c6be69788b0a4f17a92a8551a --- /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)); + } + } + +}