Integrating Drupal and SugarCRM

Part 1: In this post, we'll halfway integrate Drupal and SugarCRM. We'll have Drupal automatically create and update records in SugarCRM via SOAP web service calls.

Rather than providing yet another web to lead implementation, we'll go over a general approach for sending any data from Drupal to Sugar. We'll demonstrate populating custom SugarCRM module data as well as Sugar relationships. In general, our approach is to store data in both Drupal and SugarCRM. Drupal content we're interested in tracking will store a unique SugarCRM ID to facilitate updating the same record in Sugar if and when it is updated in Drupal. In the future, we'll write a post about having Sugar record edits trigger an update of the corresponding node(s) in Drupal.

If you just want people to submit some data that gets sent to Sugar, we recommend the Webform2Sugar module instead of all this.

Tools used for this one-way integration:

  • Drupal 6.19 (any 6.x branch should be fine)
  • SugarCRM CE 5.5.4 (older Sugar versions may have to use a version 1 of the SOAP API, this will affect your SOAP endpoint)
  • NuSOAP 0.9.5, for maximum SugarCRM compatibility
  • CCK Module - If you aren't already using it, you'll need to
  • Content Profile Module - Our preferred method of storing data about users. Mostly turns users into Drupal nodes (optional).
  • A custom Drupal module we'll write below

1. Setting up Drupal and SugarCRM content or record types

First, you'll want to get SugarCRM setup with the data you want to track coming from Drupal. In many cases, you'll just be sending over leads, contact, or accounts. In this case you're done. We'll do a little more for this tutorial.

Our Scenario: We'll be working with "Sites". For us, these are web sites we work on. You could think of them as any asset related to your customer or Drupal user. Drupal's perspective: A Site belongs to one User, and Users can have more than one Site. Sites are linked to a User's Profile through a node reference field. Sugar's perspective: A Site belongs to one Account, and an Account can have more than one Site. Sites are related to Accounts through a One to Many relationship from the primary module Sites to the related module Accounts.

Sites have a Name, a Drupal ID, and a SugarCRM ID. These turn out to be enough custom fields to give you the right approach to model almost anything in and between these two systems.

Creating Sites in Sugar:

  1. Visit Admin -> Module Builder -> New Package. Fill in the required info and hit Save.
  2. In your new Package, click New Module. Name it "Sites", choose type Basic, and save it.
  3. Find your Site module in the left side tree, go to Fields.
  4. Click add field to add our custom field we'll be working with, the Drupal Node ID. Type is textfield, field name is "site_drupal_nid", display label is "Drupal ID", check Required Field. Other defaults are fine. Click Save.
  5. Under your custom module, click Relationships. Click Add Relationship. Primary module is sites, type is "Many to One", related module is Accounts. Click Save.
  6. For Sites -> Layouts, visit all layouts and add your new custom field Drupal ID to each layout (EditView, DetailView, ListView, Available Subpanels -> Default).
  7. Click on your custom package, then click Deploy.
  8. Visit Admin -> Repair -> Rebuild Relationships, and run it. WARNING: This will remove any custom relationships you've set up in Studio. You shouldn't be doing this with live data anyway.
  9. Done. When you visit an Account in Sugar, you should see a Subpanel called "Sites". Create some test data if you want. You can also add a primary tab for Sites for easier testing if you want.

Creating Sites in Drupal:

  1. Visit Content Management -> Content Types -> Create Content Type.
  2. Name your content type site. We'll use the title of the node as the site's name. You can change "Title" to "Name" if you want.
  3. Under site, click "Manage Fields".
  4. Add a field with label "SugarCRM ID" and field name "field_site_sugarcrm_id". Type is text, text field. You can set maximum length to 36 if you want. Don't make it required, because brand new sites will not yet exist in Sugar, so we can't give them an ID yet. Sugar will give us the Sugar ID later. Save it.
  5. Add a field with label "Profile" and field name "field_site_profile_noderef". Type is Node reference, form element type is Autocomplete text field. Set the field to required, and can only reference type Profile. This can be done different ways, but basically we're creating a link between this Site node and a User. You could use User reference instead, or rely on Users to create their own nodes, in which case you wouldn't even need this relationship field and could rely on uid on the Site node instead. In any case, save it.
  6. Done. Create a couple of sites and jot down their nid's for later (hover your mouse over the Site's Edit link to get the nid).

Modifications to Users in Drupal:

We won't go over the setup and configuration of the content profile module here. What we're after is adding a field for each user that will store their corresponding Account or Contact SugarCRM record ID. This portion can be implemented in different ways, it all depends on your specific use case or business requirements. Each approach is more or less of equal difficulty. You could do it with the stock Drupal Profile module, or split Drupal Users into a Sugar Account and a Sugar Contact. For the purposes of this example, we'll simply convert Drupal Users to Sugar Accounts.

  1. Visit Content Management -> Content Types -> Profile -> Manage Fields.
  2. Add a field with label "SugarCRM ID" and field name "field_profile_sugarcrm_id". Type is text, text field. You can set maximum length to 36 if you want. Don't make it required, because brand new sites will not yet exist in Sugar, so we can't give them an ID yet. Sugar will give us the Sugar ID later.
  3. Save your new field.
  4. Done. Create a couple of users and their content profiles, and jot down their profile's nid's for later (hover your mouse over the Profile's Edit link to get the nid).

2. SOAP endpoints, a basic Drupal Module, logging in

First, we'll need to figure out our Sugar instance's SOAP endpoint. In general, this is

http://www.YourDomainOrIP.com/SugarBasePath/service/v2/soap.php?wsdl

For security, we recommend using https (SSL) once you get your certificates set up. Visit your SOAP endpoint in your browser, and you should an xml document that is your WSDL. If you don't see anything, make sure your host_name and site_url are correct in Sugar's config.php (especially if you've moved Sugar around lately).

Next, we'll get a basic Drupal module going that's capable of logging into Sugar. Don't write any code yet, you're better off downloading and customizing the module we've provided at the end of this post. First iteration of our module, which sets up a page for some integration testing and development:

  1. <?php
  2. /**
  3. * HOOK_MENU
  4. * Define a page we'll use to develop and test our integration.
  5. */
  6. function rev30_sugar_menu() {
  7. $items = array();
  8. $items['rev30_sugar/integration_test'] = array( /* defines url path */
  9. 'title' => t('SugarCRM Integration Test'),
  10. 'page callback' => 'rev30_sugar_test_page_generate', /* hook_form, function that displays page content */
  11. 'access arguments' => array('administer content types'), /* admin only */
  12. 'type' => MENU_CALLBACK, /* fires a function, not in an actual menu */
  13. );
  14. return $items;
  15. }
  16.  
  17. function rev30_sugar_test_page_generate() {
  18. $debug = true;
  19. $output = '<p>You can print some content here if you want...</p>';
  20.  
  21. $client = new stdClass;
  22. $session = _sugarLogin($client,$debug);
  23.  
  24. return $output;
  25. }

At this point, we've used hook_menu to create a page to do our integration testing, research, and development. Don't worry! We'll automate the whole thing in a minute. You'll be glad you have this page when you need to make miscellaneous SOAP calls to figure out your custom module fields and relationships.

Let's log in!

  1. function _sugarLogin(&$client = NULL, $debug = false) {
  2.  
  3. require_once(drupal_get_path('module','rev30_sugar') . '/nusoap/lib/nusoap.php');
  4.  
  5. $sugarEndpoint = 'https://www.YourDomainOrIP.com/SugarBasePath/service/v2/soap.php?wsdl';
  6.  
  7. $auth_array = array(
  8. 'user_auth' => array(
  9. 'user_name' => 'Sugar User Name Here',
  10. 'password' => md5('Sugar User Password Here'),
  11. )
  12. );
  13. $applicationInfo_array = array(
  14. 'application_name' => 'My Drupal Site!'
  15. );
  16.  
  17. $client = new nusoap_client($sugarEndpoint,true);
  18. $err = $client->getError();
  19. if ($err) {
  20. drupal_set_message('Error creating NuSoap client','error');
  21. return NULL;
  22. }
  23.  
  24. $result = $client->call('login',$auth_array,$applicationInfo_array);
  25. //$errors = _printSoapErrors($client,$result,'attempting to login',$debug); We'll get to this later
  26.  
  27. $session_id = $result['id'];
  28.  
  29. if ( $debug ) {
  30. $user_guid = $client->call('get_user_id',$session_id);
  31. drupal_set_message('Sugar Login OK? User: ' . $auth_array['user_auth']['user_name']. ' GUID: ' . $user_guid . ' Session ID: ' . $session_id . ' result: <pre>' . print_r($result,true) . '</pre>','status');
  32. }
  33.  
  34. return $session_id;
  35.  
  36. }

So we're passing our login function an empty PHP object that becomes our NuSOAP client. We're also passing a debug boolean that determines whether or not we should print a bunch of debugging stuff with drupal_set_message. Line 5 includes the NuSOAP library, stored in our module's folder.

Lines 26 is the meat of the matter- the SOAP Login call. Line 26 calls Sugar's Login web service function, passing along SugarCRM user credentials as arguments. Login returns a Session ID, which is critical information that must be passed in all proceeding web service calls, until we log out of Sugar. Line 32 is an example of a different web service function, this one gets our user's Sugar User ID for fun and debugging.

If all goes well, and you've installed and configured the module below correctly, and you visit

http://127.0.0.1/DrupalBasePath/rev30_sugar/integration_test

you'll see a Session ID that Sugar has sent us. We're in! Now we can send and receive some useful information. Let get that part set up.

3. Error Reporting

It can be critical to know if sending data to your Sugar instance has failed. We'll call this handy function after every SOAP web service request, and let it handle errors if there are any. If debug is on, we'll print visible Drupal messages. If debug is off, we'll quietly write to Drupal's logs and leave the end user oblivious.

  1. function _printSoapErrors(&$client = NULL, &$result = NULL, $errorMessage = '',$debug = FALSE ) {
  2. if ( $client != NULL && $result != NULL ) {
  3. if ($client->fault) { //Check for NuSOAP fault
  4. if ( $debug ) { drupal_set_message('soap client fault <pre>' . print_r($result,true) .'</pre>','error'); }
  5. $message = 'NuSOAP client fault! While working on ' . $errorMessage;
  6. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check SugarCRM Instance and rev30_sugar module');
  7. return true;
  8. }
  9.  
  10. $err = $client->getError();
  11. if ($err) { //NuSOAP level errors
  12. if ( $debug ) { drupal_set_message('soap client error <pre>' . print_r($err,true) .'</pre>','error'); }
  13. $message = 'NuSOAP error ' . $err . ' while working on ' . $errorMessage;
  14. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check SugarCRM Instance and rev30_sugar module');
  15. return true;
  16. } else if ( $result['error']['number'] != 0 ) { //Application level errors
  17. drupal_set_message('SugarCRM soap error: <strong>'. $result['error']['name'] .'</strong> <pre>' . print_r($result,true) .'</pre>','error');
  18. $message = 'SugarCRM soap error ' . $result['error']['name'] . ' while working on ' . $errorMessage;
  19. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check SugarCRM Instance and rev30_sugar module');
  20. return true;
  21. } else {
  22. return false; //NO ERRORS
  23. }
  24. }
  25.  
  26. //Client or result is null. Double check SOAP endpoint, Sugar's host_name and site_url
  27. $message = '_printSoapErrors: NO RESULT CAME BACK! While working on ' . $errorMessage;
  28. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  29. if ( $debug ) { drupal_set_message('NO SOAP RESULT RETURNED! ' . $errorMessage,'error'); }
  30. return true; //We have an error
  31. }

This is fairly self-documented. The first, second, and last set of error printing handle errors with the SOAP calls themselves. Lines 18-22 handle errors returned by Sugar, for example if we made calls to non-existent record types. $errorMessage is a field you are welcome to populate with the function the SOAP call was made it, to make it easier to track down SOAP errors later.

4. SugarCRM Field names, Module Relationships, get_available_modules, and get_module_fields

Before we can make SOAP calls to create sites in Sugar, we'll need to know the names of fields and relationships as far Sugar's SOAP API is concerned. This is the most difficult part of the integration and resources for learning this stuff are scarce online.

We'll use Sugar's get_available_modules function to figure out the name of our new Sites module. We'll use get_module_fields function to get the names of our custom fields and relationships on our Sites module. Wrapping these calls into functions is shown below.

Your rev30_sugar_test_page_generate function is a great place to run these PHP snippets to figure out these things (or the Devel module). We'll only need to do this once. The values and names we find will then get hardcoded into our Sites SOAP calls.

  1. function rev30_sugar_test_page_generate() {
  2. $debug = true;
  3. $output = '<p>You can print some content here if you want...</p>';
  4.  
  5. $client = new stdClass;
  6. $session = _sugarLogin($client,$debug);
  7.  
  8. _getAvailSugarModules($client,$session,$debug);
  9. //_getModuleFields($client,$session,$debug,'Accounts'); //WE'LL CALL THIS NEXT!
  10.  
  11. return $output;
  12. }
  13.  
  14. function _getAvailSugarModules(&$client = NULL, &$session = 0, $debug = false) {
  15. $result = $client->call('get_available_modules',$session);
  16. $errors = _printSoapErrors($client,$result,'getting avail sugar modules');
  17. if ( $debug ) {
  18. drupal_set_message('Available modules: <pre>' . print_r($result,true) . '</pre>');
  19. }
  20. }
  21.  
  22. function _getModuleFields(&$client = NULL, &$session = 0, $debug = false, $module = '') {
  23. $getFieldsArray = array(
  24. 'session' => $session,
  25. 'module_name' => $module
  26. );
  27. $result = $client->call('get_module_fields',$getFieldsArray);
  28. $errors = _printSoapErrors($client,$result,'getting avail sugar module fields');
  29. if ( $debug ) {
  30. drupal_set_message('Available fields on ' . $module . ':<pre>' . print_r($result,true) . '</pre>');
  31. }
  32. }

So first, we call get_available_modules, the results of which are shown to us via a Drupal message. You need to visit our module's page in Drupal to run the code. Scan through the results and search for "sites". Our Sites module came back as being called "a9582_sites". This will of course vary for you. You can see this in a few places in the Sugar UI, but by using this call, we know for sure.

Next, we'll call get_module_fields. Comment out line 10 and uncomment line 11. Change 'Accounts' to the name you received from the above call to get_available_modules. Run it. You should see a listing of fields, including the Drupal ID field we created earlier. Our field came back as 'site_drupal_nid' which is what we would expect.

Let's find the name of our custom relationship between Sites and Accounts. Looking at the result you just received, find something like this:

[3] => Array
  (
    [name] => a9582_sites_accounts
    [type] => link
    [relationship] => a9582_sites_accounts
    [module] => 
    [bean_name] => 
)

The key here is that type is link and we see the names of the two modules we have related in the name. You may see additional fields, such as a9582_sites_accounts_1 and a9582_sites_accounts_name, just ignore these.

So we're doing pretty good. We know the name of our custom module, its field names, and its relationship name for use with the SOAP API. Let's do something useful now.

5. Pushing Users from Drupal to Sugar, using set_entry

Okay, on to the fun stuff. We'll define a function called upsertProfile that's responsible for both pushing newly created Users to Sugar, as well as updating Sugar when a User gets changed in Drupal. As we mentioned in Step 1, we're storing Drupal Users and Accounts in Sugar. The approach we take here is slightly flawed as we're only dealing with changes to the User's Profile and not the User themselves. However, a change in the Profile will push changes to the User as well.

If you want to do a quick test, you can manually call this function the same way that we made calls in step 4. Just uncomment the other calls, call login, and then call

_upsertProfile($client,$session,$debug,12345);

where 12345 is one of your test Profile nodes that you created earlier.

  1. function _upsertProfile(&$client = NULL, &$session = 0, $debug = false, $profileNid = 0 ) {
  2.  
  3. if ( $debug ) { drupal_set_message('Upserting profile ' . $profileNid . '...'); }
  4.  
  5. if ( $client != NULL && $profileNid > 0 ) {
  6.  
  7.  
  8. $myProfile = node_load($profileNid,NULL,TRUE); //Load the Content Profile Node and the User
  9. if ( $myProfile->uid > 0 && $myProfile->type == 'profile' ) {
  10. $myUser = user_load(array('uid' => $myProfile->uid));
  11. } else {
  12. return NULL;
  13. if ( $debug ) { drupal_set_message('error loading profile nid ' . $profileNid,'error'); }
  14. $message = '_upsertProfile was passed nid ' . $profileNid . ' which could not be loaded by node_load.';
  15. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  16. return NULL;
  17. }
  18.  
  19. //PREPARE A SUGAR ACCOUNT RECORD
  20. $set_entry_params = array(
  21. 'session' => $session,
  22. 'module_name' => 'Accounts',
  23. 'name_value_list'=>array(
  24. array('name' => 'id','value'=>$myProfile->field_profile_sugarcrm_id[0]['value']), //If this value is not set, Sugar creates a new Account record
  25. array('name' => 'name','value'=>$myUser->name),
  26. array('name' => 'date_entered','value'=>date('Y-m-d H:i:s',$myProfile->created)), //Convert to MySQL Datetime "2010-09-14 01:12:37". Drupal stores this date as a unix timestap "1274875788".
  27. array('name' => 'email1', 'value'=>$myUser->mail),
  28. array('name' => 'account_type', 'value'=>'Customer')
  29. )
  30. );
  31.  
  32. //SEND NEW ACCOUNT TO SUGAR
  33. $result = $client->call('set_entry',$set_entry_params); //SET_ENTRY CREATES OR UPDATES A RECORD, based on whether id is passed or not
  34. $errors = _printSoapErrors($client,$result,'upserting profile node ' . $profileNid);
  35. if ( ! $errors ) {
  36. if ( $debug ) {
  37. drupal_set_message('Created/Updated Account for Profile Node ' . $profileNid . '<pre>' . print_r($result,true) . '</pre>');
  38. }
  39.  
  40. $sugarID = $result['id'];
  41. if ( strlen(utf8_decode($sugarID)) < 30 ) { //Extra sanity check
  42. if ( $debug ) { drupal_set_message('_upsertProfile: Error pushing Account to sugar. sugar did not return a sugar ID. Profile nid is ' . $profileNid . ' id returned was ' . $profileNid,'error'); }
  43. $message = "_upsertProfile: error pushing Account to sugar. sugar did not return a sugar ID. Profile nid is " . $profileNid;
  44. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  45. return NULL;
  46. }
  47.  
  48. //Update this node with its SugarID so we can update sugar data in the future. SYNC!
  49. if ( strlen($myProfile->field_profile_sugarcrm_id[0]['value']) < 2 ) {
  50. $myProfile->field_profile_sugarcrm_id[0]['value'] = $sugarID;
  51. node_save($myProfile);
  52. }
  53. }
  54. //DONE UPSERTING ACCOUNT
  55. } else {
  56. drupal_set_message('SugarCRM error: upsertProfile. Bad soap client or profile nid.','error');
  57. }
  58. }

We start of with some error checking, and make sure _upsertProfile was passed a valid soap client and Drupal profile node. Again, if you aren't using the Drupal Content Profile module, then you would do things different here. You might pass a Drupal User ID instead (but you need to track the Sugar ID somewhere!). We use the Drupal functions node_load and user_load so that we have all the information we need to send to Sugar.

At lines 20-30 we prepare an array according to the Sugar SOAP API, which is our new or updated Account object. If set_entry doesn't get passed an ID, it will create a new Sugar record. It it receives a valid Sugar record ID, the record will be updated. Makes sense and is easy.

On line 33 we make the actual SOAP call, which returns a SugarCRM record ID that we capture on line 40. Lines 49-52 update this Profile on the Drupal side, storing the Sugar record ID if it has not been set yet (if the Profile is new and hasn't been pushed to Sugar yet).

6. Pushing Sites and Relationships from Drupal to Sugar, using set_entry and set_relationship

We'll define a function called upsertSite that is responsible for both pushing newly created Sites to Sugar, as well as updating Sugar when a Site node is edited and saved on the Drupal side. This function adds setting the relationship between the SugarCRM Site and the Account. Of course, we're relying on this relationship to already exist on the Drupal side. It should have already been set at creation time, so we're set.

If you want to do a quick test, you can manually call this function the same way that we made calls in step 4. Just uncomment the other calls, call login, and then call

_upsertSite($client,$session,$debug,12345);

where 12345 is one of your test Site nodes that you created earlier.

  1. function _upsertSite(&$client = NULL, &$session = 0, $debug = false, $siteNid = 0 ) {
  2.  
  3. if ( $debug ) { drupal_set_message('Upserting site ' . $siteNid . '...'); }
  4.  
  5. if ( $client != NULL && $siteNid > 0 ) {
  6. $mySite = node_load($siteNid,NULL,TRUE); //Load the Site node
  7. if ( ! ($mySite->uid > 0 && $mySite->type == 'website') ) {
  8. if ( $debug ) { drupal_set_message('error loading site ' . $siteNid,'error'); }
  9. $message = '_upsertSite was passed site nid ' . $siteNid . ' which could not be loaded by node_load.';
  10. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  11. return NULL;
  12. }
  13.  
  14. //Find this Site's User's Profile, get its SugarID, for setting the Sugar Relationship
  15. $myAccount = node_load($mySite->field_site_profile_noderef[0]['nid'],NULL,TRUE);
  16. $myAccountSugarID = $myAccount->field_profile_sugarcrm_id[0]['value'];
  17. if ( strlen($myAccountSugarID) < 35 ) {
  18. if ( $debug ) { drupal_set_message('error loading sites account to get accounts sugar id. site nid is ' . $siteNid,'error'); }
  19. $message = "_upsertSite was passed site nid " . $siteNid . " for which we could not load the site's account's sugar id to create the relationship in Sugar.";
  20. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  21. return NULL;
  22. }
  23.  
  24. $set_entry_params = array(
  25. 'session' => $session,
  26. 'module_name' => 'a9582_sites',
  27. 'name_value_list'=>array(
  28. array('name' => 'id','value' => $mySite->field_site_sugarcrm_id[0]['value']), //blank or correct, new or update
  29. array('name' => 'name','value' => $mySite->title),
  30. array('name' => 'date_entered','value' => date('Y-m-d H:i:s',$mySite->created)) //MySQL Datetime, like "2010-09-14 01:12:37" Drupal stores as unix timestap "1274875788"
  31. )
  32. );
  33.  
  34. $result = $client->call('set_entry',$set_entry_params); //SET_ENTRY CREATES OR UPDATES A RECORD, based on whether id is passed or not
  35. $errors = _printSoapErrors($client,$result,'_upsertSite: set_entry, upserting Site node ' . $siteNid,$debug);
  36. if ( ! $errors ) {
  37. if ( $debug ) { drupal_set_message('Created/Updated Site ' . $siteNid . ' <pre>' . print_r($result,true) . '</pre>'); }
  38. $sugarID = $result['id'];
  39. if ( strlen(utf8_decode($sugarID)) < 30 ) {
  40. if ( $debug ) { drupal_set_message('Error pushing site to sugar. sugar did not return a sugar ID. Site nid is ' . $siteNid . ' id returned was ' . $sugarID . ' length is ' . strlen($sugarID),'error'); }
  41. $message = "_upsertSite: error pushing site to sugar. sugar did not return a sugar ID. Site nid is " . $siteNid;
  42. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  43. return NULL;
  44. }
  45.  
  46. //Update this node with its SugarID so we can update sugar data in the future. SYNC!
  47. if ( strlen($mySite->field_site_sugarcrm_id[0]['value']) < 2 ) {
  48. $mySite->field_site_sugarcrm_id[0]['value'] = $sugarID;
  49. node_save($mySite);
  50. }
  51. }
  52. //DONE UPSERTING SITE
  53.  
  54. //CREATE RELATIONSHIP BETWEEN NEW SITE AND ITS ACCOUNT
  55. $set_relationship_params = array(
  56. 'session' => $session,
  57. 'module_name' => 'a9582_sites', /* Custom module name, found in step 4 */
  58. 'module_id' => $sugarID, /* Sugar record ID of the site, we got from set_entry call */
  59. 'link_field_name' => 'a9582_sites_accounts', /* Custom relationship name, link type field, found in step 4 */
  60. 'related_ids' => array($myAccountSugarID) /* Sugar record ID of the Account we're linking this Site to */
  61. );
  62.  
  63. $result = $client->call('set_relationship',$set_relationship_params); //SET_ENTRY CREATES OR UPDATES A RECORD, based on whether id is passed or not
  64. $errors = _printSoapErrors($client,$result,'upserting sites account rel ' . $siteNid);
  65. if ( ! $errors ) {
  66. if ( $debug ) { drupal_set_message('Created/Updated Site to Account relationship for Site Node ' . $siteNid . '<pre>' . print_r($result,true) . '</pre>'); }
  67. }
  68. if ( $result['failed'] >= 1 ) {
  69. $message = "_upsertSite: error creating Site to Account relationship. Site nid is " . $siteNid;
  70. watchdog('rev30_sugar',$message,NULL,'WATCHDOG_ERROR','Check Sugar Instance and rev30_sugar module code');
  71. if ( $debug ) { drupal_set_message($message,'error'); }
  72. return NULL;
  73. }
  74. } else {
  75. drupal_set_message('SugarCRM error: upsertSite. Bad soap client or profile nid.','error');
  76. }
  77. }

Most of this function is similar to the one we saw above. However, on lines 15 and 16, we figure out this Site's Profile's SugarCRM record ID. Our node reference on Site stores the nid of the User's Profile. So we load the Profile node and store its SugarCRM Record ID. This ID will be used later in the function to set the relationship on the Sugar side.

The SOAP call and set_entry are basically the same as what we did above for the User Profile. After the call is made, we set the Site's Sugar record ID. However, we need an additional call to set the relationship between Site and Account in Sugar.

On line 55, we set up an array to be used with the SugarCRM SOAP API function set_relationship. module_name matches the primary module of the relationship, which is Sites. module_id is the Sugar ID of the Site we're upserting. link_field_name is the name of the relationship field we found in Step 4 above. related_ids is the ID of the Sugar Account we're linking to. We follow up this SOAP call with some additional error checking.

7. Automating the integration with Drupal hooks and hook_nodeapi

Thanks to the well designed and documented Drupal API and hooks system, we can easily automate the synchronization of Drupal and Sugar data. We rely on hook_nodeapi, which fires whenever a node is modified, created, viewed, prepared for printing, etc. We're interested in the case where nodes are updated or inserted (created). Whenever a node is updated or inserted, we want the code we wrote above to be called, so that things are pushed behind the scenes to Sugar, without any action on our part.

However, we've got a problem. When a Drupal user or admin clicks save to create a Site, hook_nodeapi gets called. This hook calls our upsertSite, which calls node_save to save the newly acquired Sugar record ID. But node_save triggers hook_nodeapi as well, so then we go through the whole process again. We end up infinite looping.

To get around this fubar, inside hook_nodeapi, we use a global variable to track which node we are currently processing. When hook_nodeapi gets called by our node_save (the second time), we pick up and this fact and stop processing this node, avoiding calling upsertProfile or upsertSite a second time.

  1. <?php
  2. /**
  3. * HOOK_NODEAPI
  4. * Respond to node creation and saves. Send data to sugar.
  5. */
  6. function rev30_sugar_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  7. switch ($op) {
  8. case 'update':
  9. case 'insert':
  10. if ( $node->type == 'site' || $node->type == 'profile' ) {
  11. $workingOn = $node->nid;
  12. if ($GLOBALS['rev30_sugar_hook_nodeapi_ran'] != $workingOn ) { //Our upsert functions call node_saves, which calls this hook again, need to prevent infinite looping
  13. $GLOBALS['rev30_sugar_hook_nodeapi_ran'] = $workingOn; //Yes, we've processed this node. Don't do again this save/update cycle
  14.  
  15. $debug = FALSE;
  16. $client = new stdClass;
  17. $session = _sugarLogin($client,$debug);
  18.  
  19. switch($node->type) {
  20. case 'site':
  21. _upsertSite($client,$session,$debug,$node->nid);
  22. break;
  23. case 'profile':
  24. _upsertProfile($client,$session,$debug,$node->nid);
  25. break;
  26. }
  27. _sugarLogout($client,$session,$debug);
  28. } //end if havent processed this node yet
  29. } //end if type we care about
  30. break;
  31. } //end hook's switch
  32. }

Line 11 starts our processing of Drupal Profile or Site node saves or updates. First we check to see if we should stop processing. If we haven't been working on this node, we'll store the nid in our global variable.

Next we call our Sugar Login web services function, call our upsert functions, and then call Logout. Pretty simple. Thank you Drupal API.

8. Putting it all together

So we've done pretty good. We've got nice functions that handle errors, node updates and creations, and we've automated everything. We know how to find our custom modules and relationships through Sugar web service functions. The Drupal watchdog system reports any errors, and Sugar is happily updated with fresh content 24/7.

9. How to use the Drupal module provided

We've attached the module we've build in the snippets above, including the NuSOAP library. Feel free to use it as a starting point for your own integration. If you want to create the node and module types like we've done above, it will work out of the box. You could also just set up the Profile part, and save Site for later or use your own content type instead. You'll just need set up a few things:

  • Change the Sugar SOAP endpoint in rev30_sugar.module, line 256, to match yours
  • Change the Sugar SOAP Login credentials in rev30_sugar.module, lines 260 and 261, to match yours

10. References

10. Troubleshooting

SOAP returns null/nothing: Double check host_name and site_url in Sugar's config.php (inside Sugar's root folder)

Thanks for sticking it out! We welcome any corrections, improvements, suggestions, criticisms, terminology improvements, etc. Add you comments below!

Comments

I finally got around to

I finally got around to trying your post, but the attachment gives me an error 404. Can you check on that file's availability?

So far, your article has been very informative. Thanks for making this available!!!

Well, I put together all the

Well, I put together all the parts of your post into a ".module" file and got that to install properly.

(NOTE: copy/pasting _all_ code snippets together ends up with two copies of "rev30_sugar_test_page_generate", causing a WSOD. Just get rid of one to continue.)

Now, when I run the "/rev30_sugar/integration_test" page, I get a "User:" value, but nothing else. Can you help me with this?

Got into Eclipse and found

Got into Eclipse and found that I was failing in _sugar_login, line 24, where we call the nuSOAP client. I get the following further in when nuSOAP is checking the WSDL. I'll continue working on this myself, but I'm documenting what I find in case someone else can benefit. Thanks.


$errstr = (string:615) Getting https://localhost:8888/sugar/service/v2/soap.php?wsdl - HTTP ERROR:
cURL ERROR: 35: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
url: https://localhost:8888/sugar/service/v2/soap.php?wsdl
http_code: 0
header_size: 0
request_size: 0
filetime: -1
ssl_verify_result: 0
redirect_count: 0
total_time: 0
namelookup_time: 0.017026
connect_time: 0.017255
pretransfer_time: 0
size_upload: 0
size_download: 0
speed_download: 0
speed_upload: 0
download_content_length: 0
upload_content_length: 0
starttransfer_time: 0
redirect_time: 0

More info. I'm dev'ing on OS

More info.

I'm dev'ing on OS X. I installed the sugarondrupal contrib module and was able to get both REST and SOAP connections, though it did require the soap contrib module.

I'm still unable to get a successful login through the approach described here, though I may merge the techniques here with the sugarondrupal module.

More later.

So, another update on this. I

So, another update on this. I got past my issues above.

Turns out my problem was simply from having "https" in my $sugarEndpoint. It just took me looking through your error handler to see the comment for why there were no results.

Onward, and thanks!

So, I'm just about complete

So, I'm just about complete with these instructions and am already chomping at the bit for Part 2, going from Sugar to Drupal. When are you going to write that, and/or are you available for providing that service in the meantime?

Thank you again for this post. It has helped me tremendously on a number of levels both to understand this Drupal/Sugar interaction, and to more fully understand Drupal itself.

Hi Burt, Glad to see someone

Hi Burt, Glad to see someone excited about this post! It sounds like you were able to work through your localhost issues.. maybe you didn't have SSL installed properly?

We're glad this post has helped you. The module download link is fixed. We don't have any immediate plans to provide Part 2 for this post (we haven't implemented it yet!), but here's something to get you started.

1. Use "hooks" in Sugar to take action when appropriate records are updated. See SugarCRM Logic Hooks. You be able to do whatever you want with PHP at the appropriate time, so you'll make web service calls like we did on the Drupal side.

2. Open up some Web Services on your Drupal Site Sugar needs to be able to call a Drupal-run web service to be able to update Drupal nodes. Check out the Drupal Services module. It looks like you'll need to write another Drupal module that provides the SOAP call functions/methods. Check out the Creating a Note Service example.

Thanks for your comment. I

Thanks for your comment. I _really_ appreciated this post.

I followed the tutorial and

I followed the tutorial and installed the module, when testing the link rev30_sugar/integration_test/ it just display the string from the code...
I modified both username/password and wsdl url...

If you've left everything as

If you've left everything as it came with the module, at the very least you should see "Sugar Login OK? User: ", the debugging stuff. Do you get that?

It sounds like your SOAP calls are not returning anything... please go over step 10 the troubleshooting and let us know.

I received this error from

I received this error from the integration test page:

# Available modules:

Array
(
[faultcode] => 11
[faultactor] =>
[faultstring] => Invalid Session ID
[detail] => The session ID is invalid
)

and also

# warning: Attempt to modify property of non-object in D:\webserver\xampp\htdocs\acquia\modules\drupalsoap\nusoap\lib\nusoap.php on line 4694.

It appears to have worked somewhat because it also has

# Sugar Login OK? User: admin GUID: Array Session ID: av4gcdfflgcrnjh68rc93beo42 result:

Any ideas of what's happening ? should it work with SugarCRM 6.0?

I'm not sure what's

I'm not sure what's happening... I'm tempted to say you're calling get_available_modules without logging in first, but you are showing a session id. You're calling both functions back to back right?

I'd do some research to make sure the method signatures (especially get_available_modules) are the same for Sugar version 6. This whole post is based on Sugar 5.x.

Keep us posted.

Thanks for the excellent

Thanks for the excellent article!

I too am having issues getting it working with the latest SugarCRM, 6.01. It works with 5.5.4. OK - well, I have it logging in and retrieving the list of available modules.

With $sugarEndpoint = 'http://sugar.mysite.com/soap.php?wsdl' I get this:

PHP Warning: Missing argument 1 for get_user_id() in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/soap/SoapSugarUsers.php on line 936
PHP Warning: Missing argument 1 for get_available_modules() in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/soap/SoapSugarUsers.php on line 865

With $sugarEndpoint = 'http://sugar.mysite.com/service/v2/soap.php?wsdl' I get this:

PHP Notice: Undefined index: action in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/modules/Users/authentication/AuthenticationController.php on line 117
PHP Warning: Missing argument 1 for SugarWebServiceImpl::get_user_id() in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/service/core/SugarWebServiceImpl.php on line 650
PHP Notice: Undefined variable: session in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/service/core/SugarWebServiceImpl.php on line 653
PHP Warning: Missing argument 1 for SugarWebServiceImpl::get_available_modules() in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/service/core/SugarWebServiceImpl.php on line 1034
PHP Notice: Undefined variable: session in /var/www/vhosts/mysite.com/subdomains/sugar/httpdocs/service/core/SugarWebServiceImpl.php on line 1038

Will update when I get any progress, any tips welcome ;)

Hi Steve, It looks like you

Hi Steve,

It looks like you are dealing with Notices and Warnings generated by the SugarCRM side. Unfortunately, this is the best we could do as well. It seems that Sugar just hasn't put much effort into clean web services. With their lack of documentation on anything but the most basic soap queries, it isn't surprising.

I'd say keep on trucking and ignore the warnings if things are working. Use v2 of Sugar's web services if you're trying to follow this article. We've had good reliability in spite of the mess spit out into the debug logs. Sorry but we haven't experimented with version 6 yet, it's still a little new.

Thanks - no, it doesn't work

Thanks - no, it doesn't work on 6 at the moment, only managed to get it working on 5.5.4. Will let you know if I get any success!

...I stand corrected - just

...I stand corrected - just got it all working on SugarCRM 6!

Seems the first time you do something it doesn't work, but then subsequently it works.

Thanks again for all the help your article has given me over the last few days, will be giving some code back - so far I've just changed your functions so I now have a 'company' content type in Drupal which creates 'accounts' in Sugar, and for profiles I have content profile with surname textfield and company noderef so you can add Drupal users as contacts within accounts on Sugar.

I usually get these kinds of

I usually get these kinds of errors when the user I am using for soap services has an expired password.

fyi - this issue: "# warning:

fyi - this issue:

"# warning: Attempt to modify property of non-object in D:\webserver\xampp\htdocs\acquia\modules\drupalsoap\nusoap\lib\nusoap.php on line 4694."

seems to be a PHP 5.2/5.3 issue. It works fine on my hosting (5.2) but shows the error on my local machine (5.3). All works fine though in spite of the error.

Try this: $getModulesArray

Try this:


$getModulesArray = array(
'session' => $session,
);

$result = $client->call('get_available_modules',$getModulesArray);

error loading sites account

error loading sites account to get accounts sugar id. site nid is 949

Hi,

This is good post but i have getting some problem.

When i am assessing "_upsertSite($client,$session,$debug,949);"

It gives me error like this

error loading sites account to get accounts sugar id. site nid is 949

Please give me some idea.

This problem was

This problem was solved....... i do one mistake module name was wrong.

I got the following error

I got the following error from the Drupal log:
_upsertProfile: error pushing Account to sugar. sugar did not return a sugar ID

What could be the reason?

Fixed previous eror, now I

Fixed previous eror, now I receive the following

_upsertSite was passed site nid 1328 which could not be loaded by node_load.