diff --git a/css/salesforce.css b/css/salesforce.css new file mode 100644 index 0000000000000000000000000000000000000000..5467576b840043eeece9f83574c514377a38d344 --- /dev/null +++ b/css/salesforce.css @@ -0,0 +1,18 @@ +form#salesforce-mapping-fields-form div.even { + /* Why doesn't this work? */ + background-color: lightgray; +} + +form#salesforce-mapping-fields-form div.field_mapping_field > .form-item { + float: left; + width: 23%; + padding-left: 1em; +} + +form#salesforce-mapping-fields-form div.field_mapping_field { + clear: left; +} + +form#salesforce-mapping-fields-form #edit-buttons { + clear: both; +} diff --git a/modules/salesforce_mapping/salesforce_mapping.module b/modules/salesforce_mapping/salesforce_mapping.module index c72a21e47a95ff0d53e8bc03393253c8ded64464..938c6ddc493b15e3dc7c98da5910543aa74a5a0f 100644 --- a/modules/salesforce_mapping/salesforce_mapping.module +++ b/modules/salesforce_mapping/salesforce_mapping.module @@ -100,9 +100,9 @@ function salesforce_mapping_load($name) { $mapping = \Drupal::entityTypeManager() ->getStorage('salesforce_mapping') ->load($name); - if (empty($mapping)) { - throw new Exception("No mapping found for $name."); - } + //if (empty($mapping)) { + // throw new Exception("No mapping found for $name."); + //} return $mapping; } @@ -124,9 +124,9 @@ function salesforce_mapping_load_multiple($properties = []) { $mappings = \Drupal::entityTypeManager() ->getStorage('salesforce_mapping') ->loadByProperties($properties); - if (empty($mappings)) { - throw new Exception('No mappings found.'); - } + //if (empty($mappings)) { + // throw new Exception('No mappings found.'); + //} return $mappings; } @@ -157,9 +157,12 @@ function salesforce_mapped_object_load_multiple($properties = []) { $mappings = \Drupal::entityTypeManager() ->getStorage('salesforce_mapped_object') ->loadByProperties($properties); - if (empty($mappings)) { - throw new Exception('No mapped objects found'); - } + // Do we really want to throw an exception here? + // What if we're trying to load an object in a process + // that needs to create a new object? + //if (empty($mappings)) { + // throw new Exception('No mapped objects found'); + //} return $mappings; } @@ -168,7 +171,7 @@ function salesforce_mapped_object_load_multiple($properties = []) { */ function salesforce_mapped_object_load_by_drupal($entity_type, $entity_id) { return salesforce_mapped_object_load_multiple([ - 'entity_type_id' => $entity_type, + 'entity_type_id' => $entity_type, 'entity_id' => $entity_id ]); } @@ -200,7 +203,10 @@ function salesforce_mapping_get_mapped_objects() { $mappings = salesforce_mapping_load_multiple(); usort($mappings, 'salesforce_mapping_sort'); foreach ($mappings as $mapping) { - $object_types[$mapping->getSalesforceObjectType()] = $mapping->$mapping->getSalesforceObjectType(); + $type = $mapping->getSalesforceObjectType(); + // @TODO Why is the assignment $mapping->$mapping? + //$object_types[$mapping->getSalesforceObjectType()] = $mapping->$mapping->getSalesforceObjectType(); + $object_types[$type] = $type; } return $object_types; } @@ -209,36 +215,10 @@ function salesforce_mapping_get_mapped_objects() { * Sort mappings by weight. */ function salesforce_mapping_sort($mapping_a, $mapping_b) { - if ($mapping_a->weight == $mapping_b->weight) { + if ($mapping_a->get('weight') == $mapping_b->get('weight')) { return 0; } - return ($mapping_a->weight < $mapping_b->weight) ? -1 : 1; -} - - -/** - * Implements hook_entity_delete(). - */ -function salesforce_mapping_entity_delete(EntityInterface $entity) { - // Avoid recursion. Mapped Objects cannot be mapped themselves. - if ($entity instanceof MappedObject) { - return; - } - - // Delete any Salesforce object mappings with this entity. - try { - $mapped_objects = salesforce_mapped_object_load_by_entity($entity); - } - catch (Exception $e) { - // No mapped objects, nothing to do. - return; - } - - // We only deal with mapped objects here. If SF records need to be deleted, - // salesforce_push will handle them. - foreach ($mapped_objects as $mapped_object) { - $mapped_object->delete(); - } + return ($mapping_a->get('weight') < $mapping_b->get('weight')) ? -1 : 1; } /** diff --git a/modules/salesforce_mapping/src/Entity/MappedObject.php b/modules/salesforce_mapping/src/Entity/MappedObject.php index d7e594ed90fcbcb3663a64c8c80d61f72211d1c7..0fc3eba258045923b3b0b09a5cebdf2a834f485d 100644 --- a/modules/salesforce_mapping/src/Entity/MappedObject.php +++ b/modules/salesforce_mapping/src/Entity/MappedObject.php @@ -7,14 +7,17 @@ namespace Drupal\salesforce_mapping\Entity; -use Drupal\Core\Entity\RevisionableContentEntityBase; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\RevisionableContentEntityBase; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Language\LanguageInterface; -use Drupal\salesforce_mapping\Entity\MappedObjectInterface; use Drupal\salesforce\SFID; +use Drupal\salesforce\SalesforceEvents; +use Drupal\salesforce_mapping\Entity\MappedObjectInterface; +use Drupal\salesforce_mapping\PushParams; +use Drupal\salesforce_mapping\SalesforcePushEvent; use Drupal\user\UserInterface; /** @@ -80,6 +83,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject * {@inheritdoc} */ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $i = 0; // We can't use an entity reference, which requires a single entity type. We need to accommodate a reference to any entity type, as specified by entity_type_id $fields['entity_id'] = BaseFieldDefinition::create('integer') ->setLabel(t('Entity ID')) @@ -163,7 +167,6 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject ->setLabel(t('Changed')) ->setDescription(t('The time that the object mapping was last edited.')) ->setRevisionable(TRUE) - ->setTranslatable(TRUE) ->setDisplayOptions('view', [ 'label' => 'above', 'type' => 'string', @@ -193,7 +196,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject ->setSetting('max_length', SALESFORCE_MAPPING_TRIGGER_MAX_LENGTH) ->setRevisionable(TRUE); - // @see ContentEntityBase::baseFieldDefinitions + // @see ContentEntityBase::baseFieldDefinitions // and RevisionLogEntityTrait::revisionLogBaseFieldDefinitions $fields += parent::baseFieldDefinitions($entity_type); @@ -239,7 +242,12 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject ->getStorage($this->entity_type_id->value) ->load($this->entity_id->value); - $params = $mapping->getPushParams($drupal_entity); + // previously hook_salesforce_push_params_alter + $params = new PushParams($mapping, $drupal_entity); + \Drupal::service('event_dispatcher')->dispatch( + SalesforceEvents::PUSH_PARAMS, + new SalesforcePushEvent($this, $params) + ); // @TODO is this the right place for this logic to live? // Cases: @@ -254,7 +262,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject $mapping->getSalesforceObjectType(), $mapping->getKeyField(), $mapping->getKeyValue($drupal_entity), - $params + $params->getParams() ); } elseif ($this->sfid()) { @@ -262,14 +270,14 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject $client->objectUpdate( $mapping->getSalesforceObjectType(), $this->sfid(), - $params + $params->getParams() ); } else { $action = 'create'; $result = $client->objectCreate( $mapping->getSalesforceObjectType(), - $params + $params->getParams() ); } @@ -297,8 +305,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject ->set('last_sync_action', 'push_delete') ->set('last_sync_status', TRUE) ->save(); - - return $result; + return $this; } public function pull(array $sf_object = NULL, EntityInterface $drupal_entity = NULL) { diff --git a/modules/salesforce_mapping/src/Entity/SalesforceMapping.php b/modules/salesforce_mapping/src/Entity/SalesforceMapping.php index 503ce28dadee22d1472cc4fa29900f3855419d1e..a5b7420bae216be849ffd7f2e067cda2642c8280 100644 --- a/modules/salesforce_mapping/src/Entity/SalesforceMapping.php +++ b/modules/salesforce_mapping/src/Entity/SalesforceMapping.php @@ -12,6 +12,8 @@ use Drupal\salesforce_mapping\SalesforceMappingFieldPluginManager; use Drupal\Core\Entity\EntityInterface; use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface; use Drupal\salesforce\Exception; +use Drupal\salesforce\SalesforceEvents; +use Drupal\salesforce_mapping\PushParams; /** * Defines a Salesforce Mapping configuration entity class. @@ -173,30 +175,6 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt return parent::save(); } - /** - * Given a Drupal entity, return an array of Salesforce key-value pairs - * - * @param object $entity - * Entity wrapper object. - * - * @return array - * Associative array of key value pairs. - * @see salesforce_push_map_params (from d7) - */ - public function getPushParams(EntityInterface $entity) { - // @TODO This should probably be delegated to a field plugin bag? - foreach ($this->getFieldMappings() as $field_plugin) { - // Skip fields that aren't being pushed to Salesforce. - if (!$field_plugin->push()) { - continue; - } - $params[$field_plugin->config('salesforce_field')] = $field_plugin->value($entity); - } - // @TODO make this an event - // drupal_alter('salesforce_push_params', $params, $mapping, $entity_wrapper); - return $params; - } - /** * Given a Salesforce object, return an array of Drupal entity key-value pairs * @@ -264,7 +242,7 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt public function getFieldMapping(array $field) { return $this->fieldManager->createInstance( $field['drupal_field_type'], - $field + $field['config'] ); } @@ -272,64 +250,13 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt } - /** - * Helper function returns boolean whether this mapping responds to - * Drupal CRUD operation(s). - * - * @return bool - */ - public function doesPush(array $ops = []) { - $ops = [ - SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE, - SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE, - SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE, - ]; - return $this->doesCrud($ops); - } - - /** - * Helper function returns boolean whether this mapping responds to - * Salesforce CRUD operation(s). - * - * @return bool - */ - public function doesPull() { - $ops = [ - SALESFORCE_MAPPING_SYNC_SF_CREATE, - SALESFORCE_MAPPING_SYNC_SF_UPDATE, - SALESFORCE_MAPPING_SYNC_SF_DELETE, - ]; - return $this->doesCrud($ops); - } - - /** - * Helper function returns boolean whether this mapping responds to given - * Salesforce or Drupal CRUD operation(s). - * - * @param array $ops (optional) - * Array containing one or more of: - * * SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE - * * SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE - * * SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE - * * SALESFORCE_MAPPING_SYNC_SF_CREATE - * * SALESFORCE_MAPPING_SYNC_SF_UPDATE - * * SALESFORCE_MAPPING_SYNC_SF_DELETE - * - * If empty, treat as if all values were provided. - * @return bool - */ - public function doesCrud(array $ops = []) { - if (empty($ops)) { - $ops = [ - SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE, - SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE, - SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE, - SALESFORCE_MAPPING_SYNC_SF_CREATE, - SALESFORCE_MAPPING_SYNC_SF_UPDATE, - SALESFORCE_MAPPING_SYNC_SF_DELETE - ]; + public function checkTriggers(array $triggers) { + foreach ($triggers as $trigger) { + if ($this->sync_triggers[$trigger] == 1) { + return TRUE; + } } - return !empty(array_intersect($ops, array_keys(array_filter($this->sync_triggers)))); + return FALSE; } } diff --git a/modules/salesforce_mapping/src/Form/MappedObjectForm.php b/modules/salesforce_mapping/src/Form/MappedObjectForm.php index 39feefba4d5ca27280b2326071d3783b3c076337..fd19d41bdce37c8a76b5ccd76c3c05df6f3a005e 100644 --- a/modules/salesforce_mapping/src/Form/MappedObjectForm.php +++ b/modules/salesforce_mapping/src/Form/MappedObjectForm.php @@ -17,6 +17,7 @@ use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\salesforce\Exception; use Drupal\salesforce_mapping\Entity\MappedObject; use Drupal\salesforce_mapping\Entity\SalesforceMapping; use Drupal\salesforce_mapping\SalesforceMappingFieldPluginInterface; @@ -106,7 +107,13 @@ class MappedObjectForm extends ContentEntityForm { } // Push to SF. - $result = $mapped_object->push(); + try { + $result = $mapped_object->push(); + } + catch (Exception $e) { + drupal_set_message(t('Push failed with an exception: %exception', array('%exception' => $e->getMessage())), 'error'); + return; + } $mapped_object ->set('salesforce_id', $result['id']) ->save(); @@ -148,7 +155,6 @@ class MappedObjectForm extends ContentEntityForm { */ public function save(array $form, FormStateInterface $form_state) { $this->getEntity()->save(); - // $this->entity->save(); drupal_set_message($this->t('The mapping has been successfully saved.')); } diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php index c1332204b70d56aa017d690fff5209215d1d8e9e..ad433ee677d97257c7c2c637154286560f4b4384 100644 --- a/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php +++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php @@ -10,12 +10,13 @@ namespace Drupal\salesforce_mapping\Form; use Symfony\Component\Debug\Debug; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Ajax\CommandInterface; use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Ajax\CommandInterface; use Drupal\Core\Ajax\InsertCommand; +use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; use Drupal\salesforce_mapping\SalesforceMappingFieldPluginInterface as FieldPluginInterface; /** @@ -30,6 +31,7 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { */ public function buildForm(array $form, FormStateInterface $form_state) { $form['#entity'] = $this->entity; + $form['#attached']['library'][] = 'salesforce/admin'; // For each field on the map, add a row to our table. $form['overview'] = ['#markup' => 'Field mapping overview goes here.']; @@ -60,7 +62,7 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { } $form['field_mappings_wrapper'] = [ - '#title' => t('Field map'), + '#title' => t('Mapped Fields'), '#type' => 'details', '#id' => 'edit-field-mappings-wrapper', '#open' => TRUE, @@ -72,18 +74,10 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { $field_mappings_wrapper['field_mappings'] = [ '#tree' => TRUE, - '#type' => 'table', + '#type' => 'container', // @TODO there's probably a better way to tie ajax callbacks to this element than by hard-coding an HTML DOM ID here. '#id' => 'edit-field-mappings', - '#header' => [ - // @TODO: there must be a better way to get two fields in the same cell than to create an extraneous column - 'drupal_field_type' => '', - 'drupal_field_type_label' => $this->t('Field type'), - 'drupal_field_value' => $this->t('Drupal field'), - 'salesforce_field' => $this->t('Salesforce field'), - 'direction' => $this->t('Direction'), - 'ops' => $this->t('Operations'), - ], + '#attributes' => ['class' => ['container-striped']], ]; $rows = &$field_mappings_wrapper['field_mappings']; @@ -94,21 +88,8 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { ], ]; - // @TODO figure out how D8 does tokens - // $form['field_mappings_wrapper']['token_tree'] = array( - // '#type' => 'container', - // '#attributes' => array( - // 'id' => array('edit-token-tree'), - // ), - // ); - // $form['field_mappings_wrapper']['token_tree']['tree'] = array( - // '#theme' => 'token_tree', - // '#token_types' => array($drupal_entity_type), - // '#global_types' => TRUE, - // ); $add_field_text = !empty($field_mappings) ? t('Add another field mapping') : t('Add a field mapping to get started'); - $form['buttons'] = ['#type' => 'container']; $form['buttons']['field_type'] = [ '#title' => t('Field Type'), @@ -134,19 +115,29 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { ], ]; - // Field mapping form. - $has_token_type = FALSE; + $row_template = [ + '#type' => 'container', + '#attributes' => ['class' => ['field_mapping_field', 'row']] + ]; // Add a row for each saved mapping + $zebra = 0; foreach ($this->entity->getFieldMappings() as $field_plugin) { - $rows[] = $this->get_row($field_plugin, $form, $form_state); + $row = $row_template; + $row['#attributes']['class']['zebra'] = ($zebra % 2) ? 'odd' : 'even'; + $rows[] = $row + $this->get_row($field_plugin, $form, $form_state); + $zebra++; } // Apply any changes from form_state to existing fields. $input = $form_state->getUserInput(); if (!empty($input['field_mappings'])) { for ($i = count($this->entity->getFieldMappings()); $i < count($input['field_mappings']); $i++) { - $rows[] = $this->get_row($this->entity->getFieldMapping($input['field_mappings'][$i]), $form, $form_state); + $row = $row_template; + $row['#attributes']['class']['zebra'] = ($zebra % 2) ? 'odd' : 'even'; + $field_plugin = $this->entity->getFieldMapping($input['field_mappings'][$i]); + $rows[] = $row + $this->get_row($field_plugin, $form, $form_state); + $zebra++; } } @@ -160,7 +151,10 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { if (!empty($form_state->getValues()) && $form_state->getValue('add') == $form_state->getValue('op') && !empty($input['field_type'])) { - $rows[] = $this->get_row(NULL, $form, $form_state); + $row = $row_template; + $row['#attributes']['class']['zebra'] = ($zebra % 2) ? 'odd' : 'even'; + $rows[] = $row + $this->get_row(NULL, $form, $form_state); + $zebra++; } // Retrieve and add the form actions array. @@ -190,9 +184,8 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { */ private function get_row(FieldPluginInterface $field_plugin = NULL, $form, FormStateInterface $form_state) { $input = $form_state->getUserInput(); - if ($field_plugin != NULL) { - $field_type = $field_plugin->config('drupal_field_type'); + $field_type = $field_plugin->getPluginId(); $field_plugin_definition = $this->get_field_plugin($field_type); } else { @@ -205,62 +198,35 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { } if (empty($field_type)) { - // @TODO throw an exception here ? - return; + throw new Exception('Invalid field type configuration'); } if (empty($field_plugin_definition)) { - // @TODO throw an exception here ? - return; + throw new Exception('No field plugin definition found for ' . $field_type); } - // @TODO allow plugins to override forms for all these fields - $row['drupal_field_type'] = [ - '#type' => 'hidden', - '#value' => $field_type, - ]; - $row['drupal_field_type_label'] = [ - '#markup' => $field_plugin_definition['label'], - ]; - - // Display the plugin config form here: - $row['drupal_field_value'] = $field_plugin->buildConfigurationForm($form, $form_state); - - $row['salesforce_field'] = [ - '#type' => 'select', - '#description' => t('Select a Salesforce field to map.'), - '#multiple' => (isset($drupal_field_type['salesforce_multiple_fields']) && $drupal_field_type['salesforce_multiple_fields']) ? TRUE : FALSE, - '#options' => $this->get_salesforce_field_options(), - '#default_value' => $field_plugin->config('salesforce_field'), - '#empty_option' => $this->t('- Select -'), - ]; - - $row['direction'] = [ - '#type' => 'radios', - '#options' => [ - SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => t('Drupal to SF'), - SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL => t('SF to Drupal'), - SALESFORCE_MAPPING_DIRECTION_SYNC => t('Sync'), - ], - '#required' => TRUE, - '#default_value' => $field_plugin->config('direction') ? $field_plugin->config('direction') : SALESFORCE_MAPPING_DIRECTION_SYNC, - ]; + $row['config'] = $field_plugin->buildConfigurationForm($form, $form_state); // @TODO implement "lock/unlock" logic here: // @TODO convert these to AJAX operations $operations = [ - 'locked' => $this->t('Lock'), + // 'locked' => $this->t('Lock'), 'delete' => $this->t('Delete') ]; $defaults = []; - if ($field_plugin->config('locked')) { - $defaults = ['lock']; - } + // if ($field_plugin->config('locked')) { + // $defaults = ['lock']; + // } $row['ops'] = [ + '#title' => t('Operations'), '#type' => 'checkboxes', '#options' => $operations, '#default_value' => $defaults, ]; + $row['drupal_field_type'] = [ + '#type' => 'hidden', + '#value' => $field_plugin->getPluginId() + ]; return $row; } @@ -273,23 +239,34 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { // indexing while removing delete field mappings. $values = $form_state->getValues(); + if (empty($values['field_mappings'])) { + // No mappings have been added, no validation to be done. + return; + } + $key = $values['key']; $key_mapped = FALSE; + foreach ($values['field_mappings'] as $i => $value) { - if ($value['salesforce_field'] == $key) { - $key_mapped = TRUE; - } // If a field was deleted, delete it! if (!empty($value['ops']['delete'])) { $form_state->unsetValue(["field_mappings", "$i"]); continue; } - $values['field_mappings'][$i]['locked'] = !empty($value['ops']['lock']); - // Remove UI crud from form state array: - $form_state->unsetValue(['field_mappings', $i, 'ops']); - $form_state->unsetValue('field_type'); + // Pass validation to field plugins before performing mapping validation. + $field_plugin = $this->entity->getFieldMapping($value); + $sub_form_state = SubformState::createForSubform($form['field_mappings_wrapper']['field_mappings'][$i], $form, $form_state); + $field_plugin->validateConfigurationForm($form['field_mappings_wrapper']['field_mappings'][$i], $sub_form_state); + + // Send to drupal field plugin for additional validation. + if ($field_plugin->config('salesforce_field') == $key) { + $key_mapped = TRUE; + } + + // @TODO what does "locked" even mean? + // $values['field_mappings'][$i]['locked'] = !empty($value['ops']['lock']); } if (!empty($key) && !$key_mapped) { @@ -299,6 +276,23 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { } + public function submitForm(array &$form, FormStateInterface $form_state) { + // Need to transform the schema slightly to remove the "config" dereference. Also trigger submit handlers on plugins. + $form_state->unsetValue(['field_type', 'ops']); + + $values = &$form_state->getValues(); + foreach ($values['field_mappings'] as $i => &$value) { + // Pass submit values to plugin submit handler. + $field_plugin = $this->entity->getFieldMapping($value); + $sub_form_state = SubformState::createForSubform($form['field_mappings_wrapper']['field_mappings'][$i], $form, $form_state); + $field_plugin->submitConfigurationForm($form['field_mappings_wrapper']['field_mappings'][$i], $sub_form_state); + + $value = $value + $value['config']; + unset($value['config'], $value['ops']); + } + parent::submitForm($form, $form_state); + } + public function field_add_callback($form, FormStateInterface $form_state) { $response = new AjaxResponse(); // Requires updating itself and the field map. @@ -316,8 +310,6 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase { } protected function get_field_plugin($field_type) { - // @TODO not sure if it's best practice to static cache definitions, or just - // get them from SalesforceMappingFieldManager each time. $field_plugins = $this->SalesforceMappingFieldManager->getDefinitions(); return $field_plugins[$field_type]; } diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php index ddb811f2a11d73577ef07834cc53782aaad508db..18fd57edb2b3f274bb8d16769d8c8e4a8bab84ca 100644 --- a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php +++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php @@ -181,7 +181,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { '#default_value' => $this->entity->get('pull_trigger_date') ? $this->entity->get('pull_trigger_date') : 'LastModifiedDate', - '#options' => $this->get_pull_trigger_options(), + '#options' => $this->get_pull_trigger_options($salesforce_object_type), ]; // @TODO either change sync_triggers to human readable values, or make them work as hex flags again. @@ -219,6 +219,10 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { + // fudge the Date Modified form values to get validation to pass on submit + if (!empty($form_state->isSubmitted())) { + $form['salesforce_object']['pull_trigger_date']['#options'] = $this->get_pull_trigger_options($form_state->getValue('salesforce_object_type')); + } parent::validateForm($form, $form_state); $entity_type = $form_state->getValue('drupal_entity_type'); @@ -239,7 +243,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { } } } - + /** * {@inheritdoc} */ @@ -263,6 +267,8 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { */ public function salesforce_record_type_callback($form, FormStateInterface $form_state) { $response = new AjaxResponse(); + // Set the trigger options based on the selected object + $form['salesforce_object']['pull_trigger_date']['#options'] = $this->get_pull_trigger_options($form_state->getValue('salesforce_object_type')); // Requires updating itself and the field map. $response->addCommand(new ReplaceCommand('#edit-salesforce-object', render($form['salesforce_object'])))->addCommand(new ReplaceCommand('#edit-salesforce-field-mappings-wrapper', render($form['salesforce_field_mappings_wrapper']))); return $response; @@ -293,7 +299,10 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { // arbitrary restriction, but otherwise there would be dozens of entities, // making this options list unwieldy. foreach ($entity_info as $info) { - if (!class_implements($info, 'FieldableEntityInterface')) { + if ( + !in_array('Drupal\Core\Entity\ContentEntityTypeInterface', class_implements($info)) || + $info->id() == 'salesforce_mapped_object' + ) { continue; } $options[$info->id()] = $info->getLabel(); @@ -358,7 +367,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { * Return form options for available sync triggers. * * @return array - * Array of sync trigger options keyed by their machine name with their + * Array of sync trigger options keyed by their machine name with their * label as the value. */ protected function get_sync_trigger_options() { @@ -376,9 +385,11 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { * Helper function which returns an array of Date fields suitable for use a * pull trigger field. * + * @param string $name + * * @return array */ - private function get_pull_trigger_options() { + private function get_pull_trigger_options($name) { $options = []; $describe = $this->get_salesforce_object(); if ($describe) { @@ -401,5 +412,4 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { return $field_type_options; } - } diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php index 9f7949270c1421eb9da73f9a09b7d67c46f529d0..25d2b9c0c2595764e2257ec329dbc255a28ec188 100644 --- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php +++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php @@ -27,11 +27,22 @@ use Drupal\salesforce_mapping\SalesforceMappingFieldPluginBase; class Constant extends SalesforceMappingFieldPluginBase { public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - return [ + $pluginForm = parent::buildConfigurationForm($form, $form_state); + + $pluginForm['drupal_field_value'] += [ '#type' => 'textfield', '#default_value' => $this->config('drupal_field_value'), '#description' => $this->t('Enter a constant value to map to a Salesforce field.'), ]; + + // @TODO: "Constant" as it's implemented now should only be allowed to be set to "Push". In the future: create "Pull" logic for constant, which pulls a constant value to a Drupal field. Probably a separate mapping field plugin. + $pluginForm['direction']['#options'] = [ + SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => $pluginForm['direction']['#options'][SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF] + ]; + $pluginForm['direction']['#default_value'] = SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF; + + return $pluginForm; + } public function value(EntityInterface $entity) { diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php index 27a4f4d0ca4c032877505517e94477ff02b03dbc..775a55adb167345d4af99b0c4ae5382c1bcf9861 100644 --- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php +++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php @@ -28,20 +28,39 @@ class Properties extends SalesforceMappingFieldPluginBase { * Implementation of PluginFormInterface::buildConfigurationForm */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $pluginForm = parent::buildConfigurationForm($form, $form_state); // @TODO inspecting the form and form_state feels wrong, but haven't found a good way to get the entity from config before the config is saved. $options = $this->getConfigurationOptions($form['#entity']); + + // Display the plugin config form here: if (empty($options)) { - return [ + $pluginForm['drupal_field_value'] = [ '#markup' => t('No available properties.') ]; } - return [ - '#type' => 'select', - '#options' => $options, - '#empty_option' => $this->t('- Select -'), - '#default_value' => $this->config('drupal_field_value'), - '#description' => $this->t('Select a Drupal field or property to map to a Salesforce field.<br />Entity Reference fields should be handled using Related Entity Ids or Token field types.'), - ]; + else { + $pluginForm['drupal_field_value'] += [ + '#type' => 'select', + '#options' => $options, + '#empty_option' => $this->t('- Select -'), + '#default_value' => $this->config('drupal_field_value'), + '#description' => $this->t('Select a Drupal field or property to map to a Salesforce field.<br />Entity Reference fields should be handled using Related Entity Ids or Token field types.'), + ]; + } + + return $pluginForm; + } + + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + $vals = $form_state->getValues(); + $config = $vals['config']; + if (empty($config['salesforce_field'])) { + $form_state->setError($form['config']['salesforce_field'], t('Salesforce field is required.')); + } + if (empty($config['drupal_field_value'])) { + $form_state->setError($form['config']['drupal_field_value'], t('Drupal field is required.')); + } } public function value(EntityInterface $entity) { diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php index 1692f46914b02b94040c4e5337788cd9b5e05ca7..8a62aee847857de61156f40e202bf1ede94d074e 100644 --- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php +++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php @@ -29,21 +29,27 @@ class RelatedIDs extends SalesforceMappingFieldPluginBase { * This is basically the inverse of Properties::buildConfigurationForm() */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $pluginForm = parent::buildConfigurationForm($form, $form_state); + // @TODO inspecting the form and form_state feels wrong, but haven't found a good way to get the entity from config before the config is saved. $options = $this->getConfigurationOptions($form['#entity']); if (empty($options)) { - return [ + $pluginForm['drupal_field_value'] += [ '#markup' => t('No available entity reference fields.') ]; } - return [ - '#type' => 'select', - '#options' => $options, - '#empty_option' => $this->t('- Select -'), - '#default_value' => $this->config('drupal_field_value'), - '#description' => $this->t('If an existing connection is found with the selected entity reference, the linked identifier will be used.<br />For example, Salesforce ID for Drupal to SF, or Node ID for SF to Drupal.<br />If more than one entity is referenced, the entity at delta zero will be used.'), - ]; + else { + $pluginForm['drupal_field_value'] += [ + '#type' => 'select', + '#options' => $options, + '#empty_option' => $this->t('- Select -'), + '#default_value' => $this->config('drupal_field_value'), + '#description' => $this->t('If an existing connection is found with the selected entity reference, the linked identifier will be used.<br />For example, Salesforce ID for Drupal to SF, or Node ID for SF to Drupal.<br />If more than one entity is referenced, the entity at delta zero will be used.'), + ]; + } + return $pluginForm; + } /** diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php index a0db2fc8533484f36111be2a9cb66963a99a6a40..94b442afe5eb513c4d8d705e391c9363e472b74c 100644 --- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php +++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php @@ -29,20 +29,28 @@ class RelatedProperties extends SalesforceMappingFieldPluginBase { * This is basically the inverse of Properties::buildConfigurationForm() */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $pluginForm = parent::buildConfigurationForm($form, $form_state); + // @TODO inspecting the form and form_state feels wrong, but haven't found a good way to get the entity from config before the config is saved. $options = $this->getConfigurationOptions($form['#entity']); + if (empty($options)) { - return [ + $pluginForm['drupal_field_value'] += [ '#markup' => t('No available entity reference fields.') ]; } - return [ - '#type' => 'select', - '#options' => $options, - '#empty_option' => $this->t('- Select -'), - '#default_value' => $this->config('drupal_field_value'), - '#description' => $this->t('Select a property from the referenced field.<br />If more than one entity is referenced, the entity at delta zero will be used.<br />An entity reference field will be used to sync an identifier, e.g. Salesforce ID and Node ID.'), - ]; + else { + $pluginForm['drupal_field_value'] += [ + '#type' => 'select', + '#options' => $options, + '#empty_option' => $this->t('- Select -'), + '#default_value' => $this->config('drupal_field_value'), + '#description' => $this->t('Select a property from the referenced field.<br />If more than one entity is referenced, the entity at delta zero will be used.<br />An entity reference field will be used to sync an identifier, e.g. Salesforce ID and Node ID.'), + ]; + } + return $pluginForm; + + } public function value(EntityInterface $entity) { diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php index 46b70340f5a5536db659df4ecf2ba24b8f82a176..2ac0279f3fffc4ed2eca4de7eaac90cfcda533db 100644 --- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php +++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php @@ -45,14 +45,24 @@ class Token extends SalesforceMappingFieldPluginBase { } public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $pluginForm = parent::buildConfigurationForm($form, $form_state); + // @TODO expose token options on mapping form: clear, callback, sanitize // @TODO expose token tree / selector // @TODO add token validation - return [ + $pluginForm['drupal_field_value'] += [ '#type' => 'textfield', '#default_value' => $this->config('drupal_field_value'), '#description' => $this->t('Enter a token to map a Salesforce field..'), ]; + + // @TODO: "Constant" as it's implemented now should only be allowed to be set to "Push". In the future: create "Pull" logic for constant, which pulls a constant value to a Drupal field. Probably a separate mapping field plugin. + $pluginForm['direction']['#options'] = [ + SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => $pluginForm['direction']['#options'][SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF] + ]; + $pluginForm['direction']['#default_value'] = SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF; + + return $pluginForm; } public function value(EntityInterface $entity) { diff --git a/modules/salesforce_mapping/src/PushParams.php b/modules/salesforce_mapping/src/PushParams.php new file mode 100644 index 0000000000000000000000000000000000000000..e95550ab70057f16620a13c718613638d692a928 --- /dev/null +++ b/modules/salesforce_mapping/src/PushParams.php @@ -0,0 +1,72 @@ +<?php + +namespace Drupal\salesforce_mapping; + +use Symfony\Component\EventDispatcher\Event; +use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface; +use Drupal\Core\Entity\EntityInterface; + +/** + * Wrapper for the array of values which will be pushed to Salesforce. + * Usable by salesforce.client for push actions: create, upsert, update + */ +class PushParams { + + protected $params; + protected $mapping; + protected $drupal_entity; + + /** + * Given a Drupal entity, return an array of Salesforce key-value pairs + * previously salesforce_push_map_params (d7) + * + * @param SalesforceMappingInterface $mapping + * @param EntityInterface $entity + * @param array $params (optional) + */ + public function __construct(SalesforceMappingInterface $mapping, EntityInterface $entity, array $params = []) { + $this->mapping = $mapping; + $this->drupal_entity = $entity; + $this->params = $params; + foreach ($mapping->getFieldMappings() as $field_plugin) { + // Skip fields that aren't being pushed to Salesforce. + if (!$field_plugin->push()) { + continue; + } + $this->params[$field_plugin->config('salesforce_field')] = $field_plugin->value($entity); + } + } + + public function getMapping() { + return $this->mapping; + } + + public function getDrupalEntity() { + return $this->drupal_entity; + } + + public function getParams() { + return $this->params; + } + + /** + * @throws Exception + */ + public function getParam($key) { + if (!array_key_exists($key, $this->params)) { + throw new Exception("Param key $key does not exist"); + } + return $this->params[$key]; + } + + public function setParams(array $params) { + $this->params = $params; + return $this; + } + + public function setParam($key, $value) { + $this->params[$key] = $value; + return $this; + } + +} diff --git a/modules/salesforce_mapping/src/SalesforceCrudEvent.php b/modules/salesforce_mapping/src/SalesforceCrudEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..fda18e5201b5c222db8f24d412570b380381635d --- /dev/null +++ b/modules/salesforce_mapping/src/SalesforceCrudEvent.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\salesforce_mapping; + +use Symfony\Component\EventDispatcher\Event; +use Drupal\salesforce_mapping\Entity; + +class SalesforceCrudEvent extends Event { + + protected $params; + protected $mapping; + protected $mapped_object; + protected $entity; + + public function __construct(EntityInterface $entity, $operation, SalesforceMappingInterface $mapping = NULL, MappedObjectInterface $mapped_object = NULL, PushParams $params = NULL) { + $this->entity = $entity; + $this->operation = $operation; + $this->mapping = $mapping; + $this->mapped_object = $mapped_object; + $this->params = $params; + } + + public function getOperation() { + return $this->operation; + } + + public function getEntity() { + return $this->entity; + } + + public function getMapping() { + return $this->mapping; + } + + public function getMappedObject() { + return $this->mapped_object; + } + + public function getParams() { + return $this->params; + } + +} diff --git a/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php b/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php index c9418eab99af780e7e5d494b8d9bb142ea886bbc..b3ef32cc2ef2efcac6e6519fae6fb0d840d9c423 100644 --- a/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php +++ b/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php @@ -103,13 +103,52 @@ abstract class SalesforceMappingFieldPluginBase extends PluginBase implements Sa return [ 'direction' => SALESFORCE_MAPPING_DIRECTION_SYNC, 'salesforce_field' => [], - 'drupal_field_type' => $this->id, + 'drupal_field_type' => $this->getPluginId(), 'drupal_field_value' => '', 'locked' => FALSE, 'mapping_name' => '', ]; } + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $pluginForm = array(); + $plugin_def = $this->getPluginDefinition(); + + // Extending plugins will probably inject most of their own logic here: + $pluginForm['drupal_field_value'] = [ + '#title' => $plugin_def['label'], + ]; + + $pluginForm['salesforce_field'] = [ + '#title' => t('Salesforce field'), + '#type' => 'select', + '#description' => t('Select a Salesforce field to map.'), + // @TODO MULTIPLE SF FIELDS FOR ONE MAPPING FIELD NOT IN USE: + // '#multiple' => (isset($drupal_field_type['salesforce_multiple_fields']) && $drupal_field_type['salesforce_multiple_fields']) ? TRUE : FALSE, + '#options' => $this->get_salesforce_field_options($form['#entity']->getSalesforceObjectType()), + '#default_value' => $this->config('salesforce_field'), + '#empty_option' => $this->t('- Select -'), + ]; + + $pluginForm['direction'] = [ + '#title' => t('Direction'), + '#type' => 'radios', + '#options' => [ + SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => t('Drupal to SF'), + SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL => t('SF to Drupal'), + SALESFORCE_MAPPING_DIRECTION_SYNC => t('Sync'), + ], + '#required' => TRUE, + '#default_value' => $this->config('direction') ? $this->config('direction') : SALESFORCE_MAPPING_DIRECTION_SYNC, + ]; + + return $pluginForm; + } + + /** * Implements PluginFormInterface::validateConfigurationForm(). */ @@ -191,4 +230,32 @@ abstract class SalesforceMappingFieldPluginBase extends PluginBase implements Sa return in_array($this->config('direction'), [SALESFORCE_MAPPING_DIRECTION_SYNC, SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL]); } + /** + * Helper to retreive a list of fields for a given object type. + * + * @param string $salesforce_object_type + * The object type of whose fields you want to retreive. + * + * @return array + * An array of values keyed by machine name of the field with the label as + * the value, formatted to be appropriate as a value for #options. + */ + protected function get_salesforce_field_options($sfobject_name) { + static $options; + if (!empty($options[$sfobject_name])) { + return $options[$sfobject_name]; + } + $sfapi = salesforce_get_api(); + $sfobject = $sfapi->objectDescribe($sfobject_name); + $sf_fields = []; + if (isset($sfobject['fields'])) { + foreach ($sfobject['fields'] as $sf_field) { + $sf_fields[$sf_field['name']] = $sf_field['label']; + } + } + asort($sf_fields); + $options[$sfobject_name] = $sf_fields; + return $sf_fields; + } + } diff --git a/modules/salesforce_mapping/src/SalesforcePushEvent.php b/modules/salesforce_mapping/src/SalesforcePushEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..57a8888a66f2ffde2efb0e4361efd12f45654550 --- /dev/null +++ b/modules/salesforce_mapping/src/SalesforcePushEvent.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\salesforce_mapping; + +use Symfony\Component\EventDispatcher\Event; +use Drupal\Core\Entity\EntityInterface; +use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface; +use Drupal\salesforce_mapping\Entity\MappedObjectInterface; + +class SalesforcePushEvent extends Event { + + protected $params; + protected $mapping; + protected $mapped_object; + protected $entity; + + public function __construct(MappedObjectInterface $mapped_object = NULL, PushParams $params = NULL) { + $this->mapped_object = $mapped_object; + $this->params = $params; + $this->entity = $params->getDrupalEntity(); + $this->mapping = $params->getMapping(); + } + + public function getEntity() { + return $this->entity; + } + + public function getMapping() { + return $this->mapping; + } + + public function getMappedObject() { + return $this->mapped_object; + } + + public function getParams() { + return $this->params; + } + +} diff --git a/modules/salesforce_pull/salesforce_pull.module b/modules/salesforce_pull/salesforce_pull.module index 6cb7ad890f43ae4d67733280b6870d4cfc7f56cc..5af230f3fd1787e21d9e44a215da4200bca4fdfa 100644 --- a/modules/salesforce_pull/salesforce_pull.module +++ b/modules/salesforce_pull/salesforce_pull.module @@ -19,22 +19,6 @@ function salesforce_pull_cron() { } } -/** - * Implements hook_queue_info(). - */ - /* -function salesforce_pull_queue_info() { - $queues['salesforce_pull'] = array( - 'title' => t('Salesforce Pull Queue'), - 'worker callback' => 'salesforce_pull_process_records', - // Set to a high max timeout in case pulling in lots of data from SF. - 'cron' => array( - 'time' => 180, - ), - ); - return $queues; -} -*/ /** * Pull updated records from Salesforce and place them in the queue. * @@ -43,7 +27,7 @@ function salesforce_pull_queue_info() { */ function salesforce_pull_get_updated_records(RestClient $sfapi) { // @TODO: Refactor all this. - return; + //return; $queue = \Drupal::queue('cron_salesforce_pull'); // Avoid overloading the processing queue and pass this time around if it's @@ -59,7 +43,7 @@ function salesforce_pull_get_updated_records(RestClient $sfapi) { // Iterate over each field mapping to determine our query parameters. foreach (salesforce_mapping_load_multiple(['salesforce_object_type' => $type]) as $mapping) { - foreach ($mapping->field_mappings as $field_map) { + foreach ($mapping->get('field_mappings') as $field_map) { // Exclude field mappings that are only drupal to SF. if (in_array($field_map['direction'], [SALESFORCE_MAPPING_DIRECTION_SYNC, SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL])) { // Some field map types (Relation) store a collection of SF objects. @@ -151,24 +135,19 @@ function salesforce_pull_get_updated_records(RestClient $sfapi) { } } -/** - * Process records in the queue. - */ -function salesforce_pull_process_records($sf_object) { - // Moved to QueueWorker plugin class PullBase -} - /** * Process deleted records from salesforce. */ function salesforce_pull_process_deleted_records(RestClient $sfapi) { + // @TODO Add back in SOAP, and use autoloading techniques + /* if (!\Drupal::moduleHandler()->moduleExists('salesforce_soap')) { salesforce_set_message('Enable Salesforce SOAP to process deleted records'); return; } module_load_include('inc', 'salesforce_soap'); $soap = new SalesforceSoapPartner($sfapi); - + */ foreach (array_reverse(salesforce_mapping_get_mapped_objects()) as $type) { $last_delete_sync = \Drupal::state()->get('salesforce_pull_delete_last_' . $type, REQUEST_TIME); @@ -178,7 +157,16 @@ function salesforce_pull_process_deleted_records(RestClient $sfapi) { $now = $now > $last_delete_sync + 60 ? $now : $now + 60; $last_delete_sync_sf = gmdate('Y-m-d\TH:i:s\Z', $last_delete_sync); $now_sf = gmdate('Y-m-d\TH:i:s\Z', $now); - $deleted = $soap->getDeleted($type, $last_delete_sync_sf, $now_sf); + //$deleted = $soap->getDeleted($type, $last_delete_sync_sf, $now_sf); + $deleted = $sfapi->apiCall( + "sobjects/$type/deleted/?start=$last_delete_sync_sf&end=$now_sf", + [], + 'GET' + ); + // Cast $deleted as object since REST is returning an array instead of + // the object the SOAP client apparantly does + $deleted = (object) $deleted; + if (!empty($deleted->deletedRecords)) { $sf_mappings = salesforce_mapping_load_multiple( ['salesforce_object_type' => $type] @@ -231,6 +219,8 @@ function salesforce_pull_process_deleted_records(RestClient $sfapi) { * sObject of the Salesforce record. * @TODO this should move into SalesforceMapping.php */ + /* + * Moved to queueworker function salesforce_pull_map_fields($field_maps, &$entity_wrapper, $sf_object) { foreach ($field_maps as $field_map) { if ($field_map['direction'] == 'sync' || $field_map['direction'] == 'sf_drupal') { @@ -264,6 +254,7 @@ function salesforce_pull_map_fields($field_maps, &$entity_wrapper, $sf_object) { } } } + */ /** * Implements hook_salesforce_push_entity_allowed() diff --git a/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php b/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php index 1c9529682d06fd72f9a1453f337e284dc8702ff7..dc6f8b3c925166a31b9eb7ab41a5b534865c953a 100644 --- a/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php +++ b/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php @@ -7,11 +7,13 @@ namespace Drupal\salesforce_pull\Plugin\QueueWorker; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Queue\QueueWorkerBase; use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\salesforce_mapping\Entity\SalesforceMapping; /** * Provides base functionality for the Salesforce Pull Queue Workers. @@ -43,12 +45,25 @@ abstract class PullBase extends QueueWorkerBase { $mapping_conditions['salesforce_record_type'] = $sf_object['RecordTypeId']; } - $sf_mappings = salesforce_mapping_load_multiple($mapping_conditions); + try { + $sf_mappings = salesforce_mapping_load_multiple($mapping_conditions); + } + catch (Exception $e) { + return; + } foreach ($sf_mappings as $sf_mapping) { // Mapping object exists? - $mapped_object = salesforce_mapped_object_load_by_sfid($sf_object['Id']); - if ($mapped_object && in_array(SALESFORCE_MAPPING_SYNC_SF_UPDATE, $sf_mapping->sync_triggers)) { + // @TODO: alex to make this work more better =] + try { + $mapped_object = salesforce_mapped_object_load_by_sfid($sf_object['Id']); + $this->doUpdate($sf_mapping, $mapped_object); + } + catch (Exception $e) { + $this->doCreate(); + } + + if (!empty($mapped_object) && $sf_mapping->checkTriggers([SALESFORCE_MAPPING_SYNC_SF_UPDATE])) { try { $entity = \Drupal::entityTypeManager() ->getStorage($mapped_object->entity_type_id->value) @@ -90,40 +105,43 @@ abstract class PullBase extends QueueWorkerBase { } } else { - if (in_array(SALESFORCE_MAPPING_SYNC_SF_CREATE, $sf_mapping->sync_triggers)) { + if ($sf_mapping->checkTriggers([SALESFORCE_MAPPING_SYNC_SF_CREATE])) { try { // Create entity from mapping object and field maps. - $entity_info = entity_get_info($sf_mapping->drupal_entity_type); + $entity_info = \Drupal::entityTypeManager()->getDefinition($sf_mapping->get('drupal_entity_type')); // Define values to pass to entity_create(). + $entity_keys = $entity_info->getKeys(); $values = []; - if (isset($entity_info['entity keys']['bundle']) && - !empty($entity_info['entity keys']['bundle'])) { - $values[$entity_info['entity keys']['bundle']] = $sf_mapping->drupal_bundle; + if (isset($entity_keys['bundle']) && + !empty($entity_keys['bundle'])) { + $values[$entity_keys['bundle']] = $sf_mapping->get('drupal_bundle'); } else { // Not all entities will have bundle defined under entity keys, // e.g. the User entity. - $values[$sf_mapping->drupal_bundle] = $sf_mapping->drupal_bundle; + $values[$sf_mapping->get('drupal_bundle')] = $sf_mapping->get('drupal_bundle'); } // See note above about flag. $values['salesforce_pull'] = TRUE; // Create entity. - $entity = entity_create($sf_mapping->drupal_entity_type, $values); + $entity = \Drupal::entityTypeManager() + ->getStorage($sf_mapping->get('drupal_entity_type')) + ->create($values); // Flag this entity as having been processed. This does not persist, // but is used by salesforce_push to avoid duplicate processing. $entity->salesforce_pull = TRUE; - $wrapper = entity_metadata_wrapper($sf_mapping->drupal_entity_type, $entity); - salesforce_pull_map_fields($sf_mapping->field_mappings, $wrapper, $sf_object); - $wrapper->save(); + //$wrapper = entity_metadata_wrapper($sf_mapping->drupal_entity_type, $entity); + $this->mapFields($sf_mapping, $entity, $sf_object); + $entity->save(); // If no id exists, the insert failed. - list($entity_id) = entity_extract_ids($sf_mapping->drupal_entity_type, $entity); - if (!$entity_id) { + //list($entity_id) = entity_extract_ids($sf_mapping->drupal_entity_type, $entity); + if (!$entity->id()) { throw new Exception('Entity ID not returned, insert failed.'); } @@ -161,4 +179,66 @@ abstract class PullBase extends QueueWorkerBase { } } } + + /** + * Map field values. + * + * @param object $sf_mapping + * Array of field maps. + * @param object $entity + * Entity wrapper object. + * @param object $sf_object + * Object of the Salesforce record. + * @TODO this should move into SalesforceMapping.php + */ + function mapFields(SalesforceMapping $sf_mapping, EntityInterface &$entity, $sf_object) { + $foo = $sf_mapping->getPullFields($entity); + $bar = $sf_mapping->get('field_mappings'); + + // Field plugin crib sheet + //$value = $sf_object[$field->get('salesforce_field')]; + //$drupal_field = $field->get('drupal_field_value'); + + foreach ($sf_mapping->getPullFields($entity) as $field_map) { + // $poop = $field_map->get('drupal_field_value'); + // $drupal_fields_array = explode(':', $field_map->get('drupal_field_value')); + // $parent = $entity; + $mapping_field_plugin_id = $field_map->get('drupal_field_type'); + $mapping_field_plugin = $this->pluginManager->create($mapping_field_plugin_id, $field_map); + + // $drupal_field_value = $field_map->get('drupal_field_value'); + + try { + $value = $mapping_field_plugin->getPullValue($entity); + } + catch (Exception $e) { + watchdog_exception('sfpull', $e); + continue; + } + + // @TODO: make this work for reference fields. There must be a better way than a semi-colon delimited string to represent this. + // It should look more like this in the future: + // $drupal_field->getValue($entity); + + // Traverse through the field_value identifier to the child-most element. Practically this is in order to fine referenced entities. Right now we're ignoring that those exist and assuming that the field will have a value. + // foreach ($drupal_fields_array as $drupal_field) { + // } + + // $fieldmap_type = salesforce_mapping_get_fieldmap_types($field_map->get('drupal_field_type')); + // $value = call_user_func($fieldmap_type['pull_value_callback'], $parent, $sf_object, $field_map); + + // Allow this value to be altered before assigning to the entity. + drupal_alter('salesforce_pull_entity_value', $value, $field_map, $sf_object); + // if (isset($value)) { + // // @TODO: might wrongly assumes an individual value wouldn't be an + // // array. + // if ($parent instanceof EntityListWrapper && !is_array($value)) { + // $parent->offsetSet(0, $value); + // } + // else { + // $parent->set($value); + // } + } + } + } } diff --git a/modules/salesforce_push/salesforce_push.module b/modules/salesforce_push/salesforce_push.module index 6297f87b8c8539c002692dcf95aca0626111d42a..adb9721b9269e076e5f920950f4b6632b9805fb0 100644 --- a/modules/salesforce_push/salesforce_push.module +++ b/modules/salesforce_push/salesforce_push.module @@ -5,8 +5,8 @@ * Push updates to Salesforce when a Drupal entity is updated. */ -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\salesforce_mapping\Entity\MappedObject; use Drupal\salesforce_mapping\Entity\MappedObjectInterface; use Drupal\salesforce_mapping\Entity\SalesforceMapping; @@ -37,7 +37,7 @@ function salesforce_push_entity_delete(EntityInterface $entity) { function salesforce_push_salesforce_push_entity_allowed(EntityInterface $entity, $op) { // Don't allow mapped objects or mappings to be pushed! // @TODO can we implement this instead with a validation constraint? This is fugly. - if ($entity instanceof MappedObjectInterface + if ($entity instanceof MappedObjectInterface || $entity instanceof SalesforceMappingInterface) { return FALSE; } @@ -54,10 +54,11 @@ function salesforce_push_salesforce_push_entity_allowed(EntityInterface $entity, * @TODO * at some point all these hook_entity_* implementations will go away. We'll * create an event subscriber class to respond to entity events and delegate - * actions to the appropriate Push procedures. Unfortunately this point seems + * actions to the appropriate Push procedures. Unfortunately this point seems * to be a very long ways away. https://www.drupal.org/node/2551893 */ function salesforce_push_entity_crud(EntityInterface $entity, $op) { + try { $mappings = salesforce_mapping_load_by_drupal($entity->getEntityTypeId()); } @@ -69,7 +70,7 @@ function salesforce_push_entity_crud(EntityInterface $entity, $op) { foreach ($mappings as $mapping) { $mapped_objects = []; $mapped_object = FALSE; - if (!$mapping->doesCrud([$op])) { + if (!$mapping->checkTriggers([$op])) { continue; } // @TODO decide whether this hook is worth moving to Events framework, and how. Should subscribers throw an exception to prevent entity sync? Return false, like so? Something else entirely? diff --git a/salesforce.libraries.yml b/salesforce.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..042c110ac06912fff473e716747c36a2837caac3 --- /dev/null +++ b/salesforce.libraries.yml @@ -0,0 +1,6 @@ +admin: + version: 0.1 + css: + layout: + css/salesforce.css: {} + diff --git a/src/Rest/RestClient.php b/src/Rest/RestClient.php index 0f34e0ba3f023c8a54516cb9791e7c0453dac487..73640158c4a8fe8cbd76b6923f8dac3b105613ae 100644 --- a/src/Rest/RestClient.php +++ b/src/Rest/RestClient.php @@ -87,7 +87,7 @@ class RestClient { } catch (RequestException $e) { // RequestException gets thrown for any response status but 2XX - $this->response = new RestResponse($e->getResponse()); + $this->response = $e->getResponse(); } if (!is_object($this->response)) { throw new Exception('Unknown error occurred during API call'); @@ -100,10 +100,10 @@ class RestClient { // throws anything but a RequestException, let it bubble up. $this->refreshToken(); try { - $this->response = $this->apiHttpRequest($path, $params, $method); + $this->response = new RestResponse($this->apiHttpRequest($path, $params, $method)); } catch (RequestException $e) { - $this->response = new RestResponse($e->getResponse()); + $this->response = $e->getResponse(); throw $e; } break; @@ -601,7 +601,7 @@ class RestClient { * @addtogroup salesforce_apicalls */ public function objectRead($name, $id) { - return new SObject($this->apiCall("sobjects/{$name}/{$id}", [], 'GET')); + return new SObject($this->apiCall("sobjects/{$name}/{$id}")); } /** diff --git a/src/SalesforceEvents.php b/src/SalesforceEvents.php new file mode 100644 index 0000000000000000000000000000000000000000..6352393ce256b38874c21bc31648ca0503256006 --- /dev/null +++ b/src/SalesforceEvents.php @@ -0,0 +1,56 @@ +<?php + +namespace Drupal\salesforce; + +/** + * Defines events for Salesforce + * + * @see \Drupal\salesforce\SalesforceEvent + */ +final class SalesforceEvents { + + /** + * Name of the event fired when building params to push to Salesforce. + * + * This event allows modules to add, change, or remove params before they're + * pushed to Salesforce. The event listener method receives a + * \Drupal\salesforce\SalesforceEvent instance. + * Previously hook_salesforce_push_params_alter() + * + * @Event + * + * @var string + */ + const PUSH_PARAMS = 'salesforce.push_params'; + + /** + * hook_salesforce_push_entity_allowed + */ + const PUSH_CRUD_ALLOWED = 'salesforce.push_crud.allowed'; + + /** + * hook_salesforce_push_success + */ + const PUSH_SUCCESS = 'salesforce.push_success'; + + /** + * hook_salesforce_push_fail + */ + const PUSH_FAIL = 'salesforce.push_fail'; + + /** + * hook_salesforce_pull_entity_presave + */ + const PULL_PRESAVE = 'salesforce.pull_presave'; + + /** + * hook_salesforce_pull_entity_insert + */ + const PULL_INSERT = 'salesforce.pull_insert'; + + /** + * hook_salesforce_pull_entity_update + */ + const PULL_UPDATE = 'salesforce.pull_update'; + +}