Eloqua PHP SDK

I’m pleased to announce the first version of my Eloqua PHP SDK, hosted on Google Code at http://code.google.com/p/eloqua-php-sdk/, and released under the Apache License, version 2.0. Included in the tarball is example PHP code for every method call in version 1.2 of the Eloqua API, and the EXAMPLES file is also available here.

[Post to Twitter] Tweet This Post 

Catching Salesforce Exceptions

Pretty simple, but for those new to PHP or to exception handling:

// instantiate a SOAP connection to Salesforce
// catch Salesforce exception if there is one
try {
  $crmHandle->createConnection(SALESFORCE_WSDL);
} catch (Exception $e) {
  // handle exception - did you set the WSDL path above?

  // print out the exception string
  echo $e->getMessage();
}

If an exception occurs, and it’s not caught, it behaves more or less like a fatal error. PHP’s set_exception_handler() function provides a nice fallback, allowing you to avoid try/catching every method call. That’s pretty much all there is to it.

[Post to Twitter] Tweet This Post 

How to Use the Entire Salesforce API, Part Three (Describe Calls, Enterprise and Partner WSDL Formats)

Here is part three of the series documenting the Salesforce API when used with the PHP Toolkit, where we’re going to cover the method calls under the ‘Describe Calls’ section of the API documentation. Note that this post applies to both the Enterprise and Partner WSDL formats.

Prerequisites
All examples below assume the Toolkit has been instantiated and the connection has been made:

Enterprise WSDL
$crmHandle = new SforceEnterpriseClient();
$crmHandle->createConnection(‘/path/to/enterprise.wsdl.xml’);

Partner WSDL
$crmHandle = new SforcePartnerClient();
$crmHandle->createConnection(’/path/to/partner.wsdl.xml’);

Additionally, all examples except login() assume you are logged in, using a call such as
$crmHandle->login(‘joe@example.com.sbox1′, ‘*passwordhere*’ . ‘*securitytokenhere*’);

Variable Names

  • $crmHandle – reference to an instance of the Toolkit object
  • $result – the result of a Salesforce method call

Notes
Date formats are in the SOAP dateTime format (ISO 8601), e.g. ‘2009-06-01T23:01:01Z’.

If you see an error like ‘INVALID_TYPE: Must send a concrete entity type’, most likely, you have forgotten to wrap your object in an array before passing it to a method.


describeGlobal()

$result = $crmHandle->describeGlobal();

describeLayout()

$result = $crmHandle->describeLayout('Lead', '*A VALID LEAD ID HERE*');

describeSObject()
NOTE: This method call is deprecated in favor of describeSObjects().

$result = $crmHandle->describeSObject('Lead');

describeSObjects()

$result = $crmHandle->describeSObjects(array('Lead'));

describeSoftphoneLayout()
NOTE: From the API docs: ‘Use only in the context of Salesforce CRM Call Center; do not call directly from client programs.’

describeTabs()

$result = $crmHandle->describeTabs();

[Post to Twitter] Tweet This Post 

How to Use the Entire Salesforce API, Part Two (Core Calls, Partner WSDL format)

These posts are modeled after the EXAMPLES file that ships with the Salesforce Python Toolkit, in which I wrote snippets of code on how to use all the method calls in the Salesforce API spec. The posts will be modeled after the four sections of the Salesforce API docs; so they will be ‘Core Calls’, ‘Describe Calls’, ‘Utility Calls’, and ‘SOAP Headers’. There will be an Enterprise and Partner WSDL version of each.

NOTE: These examples are not intended to teach the Salesforce API; rather, they are here to illustrate how the PHP Toolkit implements the Salesforce API.

Prerequisites
All examples below assume the Toolkit has been instantiated and the connection has been made.

$crmHandle = new SforcePartnerClient();
$crmHandle->createConnection(‘/path/to/partner.wsdl.xml’);

Additionally, all examples except login() assume you are logged in, using a call such as
$crmHandle->login(‘joe@example.com.sbox1′, ‘*passwordhere*’ . ‘*securitytokenhere*’);

Conventions
<create lead> – implies that you copy and paste the code from create(), so you have a Lead in the variable $lead and a SaveResult in the variable $result

<create 2 leads> – same as create lead, except copy and paste the code from ‘create 2 leads’ in create()

Variable Names

  • $crmHandle – reference to an instance of the Toolkit object
  • $result – the result of a Salesforce method call
  • $lead – a lead object

Notes
Date formats are in the SOAP dateTime format (ISO 8601), e.g. ‘2009-06-01T23:01:01Z’.

If you see an error like ‘INVALID_TYPE: Must send a concrete entity type’, most likely, you have forgotten to wrap your object in an array before passing it to a method.


convertLead()

<create lead>
$leadConvert = new SObject();
$leadConvert->leadId = $result->id;
// The possible values for convertedStatus depend on what's in the
// picklist for your org.  You can take a look at the 'Convert Lead' screen
// in the UI for your API user to see what's there
$leadConvert->convertedStatus = 'Qualified';
$leadConvert->doNotCreateOpportunity = true;
$leadConvert->overwriteLeadSource = false;
$leadConvert->sendNotificationEmail = false;
$result = $crmHandle->convertLead($leadConvert);

create()

$lead = new SObject();
$lead->fields = array('FirstName' => 'Joe',
                      'LastName' => 'Moke',
                      'Company' => 'Jamoke, Inc.',
                      'Email' => 'joe@example.com');
$lead->type = 'Lead';
$result = $crmHandle->create(array($lead), 'Lead');

delete()

<create lead>
$result = $crmHandle->delete(array($result->id));

emptyRecycleBin()

$result = $crmHandle->emptyRecycleBin(array('*ID OF A DELETED OBJECT HERE*'));

getDeleted()

// $startDate must be the later of 30 days prior and the last recycle bin purge
$result = $crmHandle->getDeleted('Lead', $startDate, $endDate);

getUpdated()

// $startDate must be later than 30 days prior
$result = $crmHandle->getUpdated('Lead', $startDate, $endDate);

invalidateSessions()
BEWARE there is a bug in versions 13.0 and earlier of the PHP Toolkit where it calls both invalidateSessions() AND logout(), generating an exception. I would strongly discourage use of either call, as they log out all concurrent sessions of the user!

You were warned…

$result = $crmHandle->invalidateSessions($crmHandle->getSessionId());

login()

$crmHandle->login('joe@example.com.sbox1', '*password*' . '*securitytoken*');

logout()
SEE NOTES FOR invalidateSessions()

$result = $crmHandle->logout();

merge()

// assume we have two IDs stored in $id and $id2

// retrieve what will be the master record
$result = $crmHandle->retrieve('FirstName, LastName, Company, Email', 'Lead', $id);
$lead = $result[0];

$mergeRequest = new SObject();
$mergeRequest->masterRecord = $lead;
$mergeRequest->recordToMergeIds = $id2;
$result = $crmHandle->merge($mergeRequest, 'Lead');

process()
The Toolkit diverges slightly from the API here, as it doesn’t implement process(), but instead implements processSubmitRequest() and processWorkitemRequest().

processSubmitRequest() (non-standard)
NOTE: The API docs for this call are currently incorrect; the ProcessSubmitRequest object takes a property ‘comments’, not ‘comment’ as stated here.

$processRequest = new SObject();
$processRequest->objectId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*';
$processRequest->comments = 'This is what I think.';
$result = $crmHandle->processSubmitRequest(array($processRequest));

processWorkitemRequest() (non-standard)
NOTE: The API docs for this call are currently incorrect; the ProcessWorkitemRequest object takes a property ‘comments’, not ‘comment’ as stated here.

$processRequest = new SObject();
$processRequest->action = 'Approve';
$processRequest->workitemId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*';
$processRequest->comments = 'I approved this request.';
$result = $crmHandle->processWorkitemRequest(array($processRequest));

query()

$result = $crmHandle->query('SELECT FirstName, LastName FROM Lead');

queryAll()
This method is broken in the Partner version as of version 13.1; however, there is a simple fix. Add the following method to SforcePartnerClient.php at line 158:

  public function queryAll($query) {
    return new QueryResult(parent::queryAll($query));
  }

Otherwise, for the following query:

$result = $crmHandle->queryAll('SELECT FirstName, LastName FROM Lead LIMIT 2');

the Toolkit returns

    [0]=>
    object(stdClass)#6 (3) {
      ["type"]=>
      string(4) "Lead"
      ["Id"]=>
      NULL
      ["any"]=>
      string(73) "<sf:FirstName>Joe</sf:FirstName><sf:LastName>Moke</sf:LastName>"
    }

instead of

    [0]=>
    object(SObject)#5 (2) {
      ["type"]=>
      string(4) "Lead"
      ["fields"]=>
      object(stdClass)#9 (2) {
        ["FirstName"]=>
        string(4) "Joe"
        ["LastName"]=>
        string(5) "Moke"
      }
    }

like the Enterprise version does. The working example is the query above.

queryMore()

// return a maximum of 200 results
$queryOptions = new QueryOptions(200);
$crmHandle->setQueryOptions($queryOptions);
$result = $crmHandle->query('SELECT FirstName, LastName FROM Lead');
while ($result->done === false) {
  $result = $crmHandle->queryMore($result->queryLocator);
  // do something with results
}

retrieve()
NOTE: The Partner version will return a 1-length array with the object in it for a single match, the Enterprise version will return the object itself.

$result = $crmHandle->retrieve('FirstName, LastName, Company, Email', 'Lead', $id);
$lead = $result[0];

search()
Search is currently broken in the Partner Toolkit as of version 13.1, and in order to use it, you must make a couple of modifications to SforcePartnerClient.php. Note that this is not the official implementation, but it does comply with what’s in the partner WSDL.

In the class SforcePartnerClient, after retrieve(), add the following method:

  public function search($searchString) {
    return new SearchResult(parent::search($searchString));
  }

Below the class SforcePartnerClient, add the following class:

class SearchResult {
  public $searchRecords = array();

  /**
   * SearchResult ctor
   *
   * SforceBaseClient::search() will return three different structures depending on result count,
   * distinguishable in the following way:
   * 0 results: empty stdClass object
   * 1 result: $response->searchRecords is an object, not an array
   * 2+ results: $response->searchRecords is an array
   *
   * @param stdClass $response  Response from base class
   */
  public function __construct($response) {
    if (!isset($response->searchRecords)) {
      return;
    } elseif (!is_array($response->searchRecords)) {
      $this->searchRecords[]->record = new SObject($response->searchRecords->record);
    } else {
      foreach ($response->searchRecords as $searchRecord) {
        $this->searchRecords[]->record = new SObject($searchRecord->record);
      }
    }
  }
}

You can now use the search() method in the same way as in the Enterprise client:

$result = $crmHandle->search('FIND {Joe Moke} IN Name Fields RETURNING Lead(Name, Phone)');

undelete()

$result = $crmHandle->undelete(array('*ID HERE*'));

update()
NOTE: Unlike the Enterprise version, you can set more than one field to null in an update() call. However, you must unset() each field as well, not set the field equal to null or ”.

<create lead>

$lead->Id = $result->id;
$lead->fieldsToNull = array('Email', 'Company');
unset($lead->fields['Email']);
unset($lead->fields['Company']);
$crmHandle->update(array($lead), 'Lead');

upsert()

<create lead>

$lead->Id = $result->id;
$lead->FirstName = 'Bob';
$crmHandle->upsert('Id', array($lead), 'Lead');

[Post to Twitter] Tweet This Post 

How to Use the Entire Salesforce API, Part One (Core Calls, Enterprise WSDL format)

These posts are modeled after the EXAMPLES file that ships with the Salesforce Python Toolkit, in which I wrote snippets of code on how to use all the method calls in the Salesforce API spec. The posts will be modeled after the four sections of the Salesforce API docs; so they will be ‘Core Calls’, ‘Describe Calls’, ‘Utility Calls’, and ‘SOAP Headers’. There will be an Enterprise and Partner WSDL version of each.

NOTE: These examples are not intended to teach the Salesforce API; rather, they are here to illustrate how the PHP Toolkit implements the Salesforce API.

Prerequisites
All examples below assume the Toolkit has been instantiated and the connection has been made.

$crmHandle = new SforceEnterpriseClient();
$crmHandle->createConnection(‘/path/to/enterprise.wsdl.xml’);

Additionally, all examples except login() assume you are logged in, using a call such as
$crmHandle->login(‘joe@example.com.sbox1′, ‘*passwordhere*’ . ‘*securitytokenhere*’);

Conventions
<create lead> – implies that you copy and paste the code from create(), so you have a Lead in the variable $lead and a SaveResult in the variable $result

<create 2 leads> – same as create lead, except copy and paste the code from ‘create 2 leads’ in create()

Variable Names

  • $crmHandle – reference to an instance of the Toolkit object
  • $result – the result of a Salesforce method call
  • $lead – a lead object

Notes
Date formats are in the SOAP dateTime format (ISO 8601), e.g. ‘2009-06-01T23:01:01Z’.

If you see an error like ‘INVALID_TYPE: Must send a concrete entity type’, most likely, you have forgotten to wrap your object in an array before passing it to a method.


convertLead()

<create lead>
$leadConvert = new stdClass();
$leadConvert->leadId = $result->id;
// The possible values for convertedStatus depend on what's in the
// picklist for your org.  You can take a look at the 'Convert Lead' screen
// in the UI for your API user to see what's there
$leadConvert->convertedStatus = 'Qualified';
$leadConvert->doNotCreateOpportunity = true;
$leadConvert->overwriteLeadSource = false;
$leadConvert->sendNotificationEmail = false;
$result = $crmHandle->convertLead($leadConvert);

create()

$lead = new stdClass();
$lead->FirstName = 'Joe';
$lead->LastName = 'Moke';
$lead->Company = 'Jamoke, Inc.';
$lead->Email = 'joe@example.com';
$result = $crmHandle->create(array($lead), 'Lead');

delete()

<create lead>
$result = $crmHandle->delete(array($result->id));

emptyRecycleBin()

$result = $crmHandle->emptyRecycleBin(array('*ID OF A DELETED OBJECT HERE*'));

getDeleted()

// $startDate must be the later of 30 days prior and the last recycle bin purge
$result = $crmHandle->getDeleted('Lead', $startDate, $endDate);

getUpdated()

// $startDate must be later than 30 days prior
$result = $crmHandle->getUpdated('Lead', $startDate, $endDate);

invalidateSessions()
BEWARE there is a bug in versions 13.0 and earlier of the PHP Toolkit where it calls both invalidateSessions() AND logout(), generating an exception. I would strongly discourage use of either call, as they log out all concurrent sessions of the user!

You were warned…

$result = $crmHandle->invalidateSessions($crmHandle->getSessionId());

login()

$crmHandle->login('joe@example.com.sbox1', '*password*' . '*securitytoken*');

logout()
SEE NOTES FOR invalidateSessions()

$result = $crmHandle->logout();

merge()

// assume we have two IDs stored in $id and $id2

// retrieve what will be the master record
$lead = $crmHandle->retrieve('FirstName, LastName, Company, Email', 'Lead', $id);

$mergeRequest = new stdClass();
$mergeRequest->masterRecord = $lead;
$mergeRequest->recordToMergeIds = $id2;
$result = $crmHandle->merge($mergeRequest, 'Lead');

process()
The Toolkit diverges slightly from the API here, as it doesn’t implement process(), but instead implements processSubmitRequest() and processWorkitemRequest().

processSubmitRequest() (non-standard)
NOTE: The API docs for this call are currently incorrect; the ProcessSubmitRequest object takes a property ‘comments’, not ‘comment’ as stated here.

$processRequest = new stdClass();
$processRequest->objectId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*';
$processRequest->comments = 'This is what I think.';
$result = $crmHandle->processSubmitRequest(array($processRequest));

processWorkitemRequest() (non-standard)
NOTE: The API docs for this call are currently incorrect; the ProcessWorkitemRequest object takes a property ‘comments’, not ‘comment’ as stated here.

$processRequest = new stdClass();
$processRequest->action = 'Approve';
$processRequest->workitemId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*';
$processRequest->comments = 'I approved this request.';
$result = $crmHandle->processWorkitemRequest(array($processRequest));

query()

$result = $crmHandle->query('SELECT FirstName, LastName FROM Lead');

queryAll()
NOTE: Using the QueryOptions header in conjunction with queryAll() (not query() or queryMore()) DOES NOT WORK as of version 13.1, even though it should per the API spec. This is because the Toolkit fails to attach the SOAP header correctly, and to fix this, simply add

$call == "queryAll" ||

at line 266 in SforceBaseClient.php.

$result = $crmHandle->queryAll('SELECT FirstName, LastName FROM Lead LIMIT 2');
foreach ($result->records as $record) {
  echo $record->FirstName . ' ' . $record->LastName . "\n";
}

queryMore()

// return a maximum of 200 results
$queryOptions = new QueryOptions(200);
$crmHandle->setQueryOptions($queryOptions);
$result = $crmHandle->query('SELECT FirstName, LastName FROM Lead');
while ($result->done === false) {
  $result = $crmHandle->queryMore($result->queryLocator);
  // do something with results
}

retrieve()

$lead = $crmHandle->retrieve('FirstName, LastName, Company, Email', 'Lead', $id);

search()

$result = $crmHandle->search('FIND {Joe Moke} IN Name Fields RETURNING Lead(Name, Phone)');

undelete()

$result = $crmHandle->undelete(array('*ID HERE*'));

update()
NOTE: There is a bug in the Toolkit (as of v13.1) where when setting fields to null (using $obj->fieldsToNull()), you cannot pass an array of fields, you are only able to set one single field to null per update() call.

<create lead>

$lead->Id = $result->id;
$lead->fieldsToNull = 'Email';
$crmHandle->update(array($lead), 'Lead');

upsert()
NOTE: There is a VERY bad bug in versions 13.0 and earlier of the PHP Toolkit, it has ‘Contact’ hard-coded in the upsert() call in SforceEnterpriseClient.php (line 90). For it to be usable, it needs to take a type, so the method should be rewritten as follows (this will bring it in line with newer versions):

  public function upsert($ext_Id, $sObjects, $sObjectType) {
    $arg = new stdClass;
    $arg->externalIDFieldName = new SoapVar($ext_Id, XSD_STRING, 'string', 'http://www.w3.org/2001/XMLSchema');
    foreach ($sObjects as &$sObject) {
      $sObject = new SoapVar($sObject, SOAP_ENC_OBJECT, $sObjectType, $this->namespace);
    }
    $arg->sObjects = $sObjects;
    return parent::_upsert($arg);
  }

After you change that, you can use upsert() as follows:

<create lead>

$lead->Id = $result->id;
$lead->FirstName = 'Bob';
$crmHandle->upsert('Id', array($lead), 'Lead');

[Post to Twitter] Tweet This Post 

Thanks!

In the month or so since I’ve started this blog, I’ve had visitors from 50 countries. I hope some of you have found it helpful!

[Post to Twitter] Tweet This Post 

Salesforce Python Toolkit

I know this is not PHP-related, but I’ve released the first version of my Salesforce Python Toolkit, hosted by Google Code at http://code.google.com/p/salesforce-python-toolkit/. It’s released under the GNU Lesser General Public License (LGPL). Included in the tarball is an example for every method call and SOAP header in version 16.0 of the Salesforce API, so it might not be a bad reference even if you’re not developing in Python. In the course of writing this, I discovered quite a few discrepancies between the PHP Toolkit’s API implementation and the spec, which I’ll detail in a future post. Some were a result of the PHP Toolkit having been written against the 13.0 API, and some weren’t.

[Post to Twitter] Tweet This Post 

Salesforce Gotchas and Undocumented ‘Features’

Today we’re going to go through some things that I hope you find interesting and informative, things that hopefully will make your development process a lot easier. With many of the following issues, I’ve found the documentation either sparse, obscured, or non-existent.

- Objects (such as individual leads or contacts) have either a 15-character or an 18-character case-sensitive ID (e.g. 00Q6000000OaR87). You should not count on one format or the other being returned. The two formats can normally be used interchangeably (you can pass either version to an API call), but one notable exception to this rule is the Excel Connector, which integrates Salesforce and Excel. The 18-character version has some extra non-essential metadata about the object that’s not present in the earlier 15-character version. CORRECTION: This is wrong. The lesson here is that when your motivation for writing a blog is your horrible experience with your Salesforce integrators, you shouldn’t take what they say as gospel. David Schach pointed out that the 15-character version is simply case-sensitive. There is no metadata whatsoever. I apologize again for failing to fact-check this.

- Object IDs have a three-character prefix, which will be consistent for standard objects and will differ between instances for custom objects. This is not very well documented, and in fact, a Google search for “salesforce id prefix” returns no results (except this entry, after it gets indexed). If you don’t yet have any objects of a particular type, but need the ID, you can get it from the URL after clicking on the tab (or the ‘>’ to the right of the tabs if you don’t see the object you’re looking for). The URL will be something like ‘https://cs1.salesforce.com/00Q/o’, and the prefix is between the slashes (here, ‘00Q,’ for leads). Prefixes for common standard objects include:

  • 00Q (Leads)
  • 003 (Contacts)
  • 001 (Accounts)
  • 006 (Opportunities)
  • 701 (Campaigns)
  • 500 (Cases)

- Salesforce API logins (including logins from other third-party applications, like the Outlook tool) have the concept of a security token, which can be reset by going to Setup > My Personal Information > Reset My Security Token.

- Security tokens are appended to passwords when logging in via the API, so you might login with the username ‘joe’ and the password ‘mypasswordmytoken’.

- Security tokens change automatically when a password changes – watch out for this!

- There are no default administrator profiles whose passwords do not expire. This means that your API user’s password may expire, and your entire API integration will suddenly fail. I highly recommend making a new administrator profile called something like ‘Admins – Passwords Do Not Expire’ and adding your API and other back-end users to it. The original profile will not (currently) let you check the ‘Passwords do not expire’ checkbox.

- Avoid using the logout() call. As discussed before, all logins for a single user share a session id. Calling logout() will log out all concurrent connections for that Salesforce user via the API. Note also that this behavior is undocumented. Not to worry, the PHP Toolkit gracefully cleans up the SOAP connection for you.

- Deleted and merged data (e.g. deleted leads) can wreak havoc if your application relies on valid Salesforce IDs. Just because a lead exists today doesn’t mean Sales isn’t going to mass-delete those 10,000 ‘junk’ leads tomorrow.

- Salesforce objects are always referred to in the API in the singular form, so ‘Lead’ rather than ‘Leads.’

- Custom fields append __c to the field name, e.g. My_Custom_Field__c.

- There are a couple of Salesforce expressions that I’ll use here to be consistent, including ‘picklist’ to mean an HTML select element, ‘dependent picklist’ to mean a picklist whose appearance or values depend on another picklist, ‘auto number’ to mean an auto-increment field, and ‘upsert’ to mean a call that will update if a record with the specified ID exists, or else insert a new record.

Lastly, a quick aside about languages and operating systems. The code examples presented here assume a POSIX-compliant OS, such as Linux, but Windows-specific considerations should pretty much non-existent. PHP is fairly OS-agnostic for what we’re going to be doing, but inevitably there will be an OS-specific issue that will creep in. As far as portable paths go, PHP lets you use ‘/’ as a path separator in scripts on all OSes, but you could certainly instead use the native (and portable) DIRECTORY_SEPARATOR constant or ‘\\’ in the paths if you prefer. Lastly, in this blog, ‘native’ is taken to mean ‘native to PHP, written in C’ – an example of this is the native SOAP client (versus the much slower NuSOAP client written in PHP).

[Post to Twitter] Tweet This Post 

Creating an Edit-Only Workflow Rule in Salesforce

Ever wonder why there is no option to make a workflow rule fire only when an object is edited, and not created? So did I. I’m not sure of the answer to the ‘why’, but I can tell you how to get around this limitation. Simply create a rule like:

‘Case: Created By not equal to null’

This works because workflow rules fire in a time frame that happens after Salesforce receives the HTTP request, but before it saves the data to the database and updates the metadata pertaining to the object (including CreatedBy).

[Post to Twitter] Tweet This Post 

Creating a Lead via the Salesforce.com API

In our next installment, we’re going to take a closer look the lead generation code from the end the first post, as well as finish getting your SFDC development environment set up.

Choosing the Right WSDL For You and Your Organization

Salesforce uses SOAP as its Web Services protocol, and SOAP uses an XML file called a WSDL (Web Services Definition Language) file that defines how a client (our code) can interact with a server (Salesforce). For our purposes, there are two different types of WSDL files, Enterprise and Partner. Here are the specifics:

Enterprise WSDL

  • Is strongly typed
  • Contains the metadata about all standard and custom fields and objects
  • Can only be used against your Salesforce instance

Partner WSDL

  • Is loosely typed
  • Takes an array of key-value pairs
  • Does not contain metadata about objects and fields
  • Can be used against many Salesforce.com organizations

In our organization, we use the Enterprise WSDL, and the main reason why is that we like having a reference of all custom fields and objects readily available in the XML. Having the type of every field right there next to the field name at hand is nice as well. This being said, if we had a need to interact with many Salesforce.com instances, we might choose to use the Partner WSDL instead.

For the purposes of this blog, we’re generally going to be writing against the Enterprise WSDL, but our first example will be presented at the end in an ‘Enterprise’ version and a ‘Partner’ version. Those of you who find you prefer the Partner WSDL should be able to take the Partner example and adapt it in the future very easily.

Generating the WSDL

Each instance of Salesforce (each sandbox and production instance) will have their own corresponding WSDL file. It can be generated by going to Setup > Develop > API > Generate Enterprise WSDL (or Generate Partner WSDL) and downloading the generated XML. I prefer to keep the default name and put each file in a dev/, staging/, and production/ folder, but you could just as easily call the files dev.enterprise.wsdl.xml, or something to that effect.

Taking a Look at the Lead Generation Code

Now that we’re set up with our WSDL, let’s take a look at the code at the bottom of the previous post. I have removed the logout() call, after I read (and subsequently validated) this absolutely terrific article that pointed out that a user’s API connections all get the same session ID! This means that logout() actually invalidates all of the concurrent connections for that user. This is somewhat of an edge case for us, as page load times are generally very short, but all the same, we won’t be using the logout() call going forward. If you’re wondering, logging out through the Salesforce API does not log the same user out of any API connections. At any rate, here is the code, slightly modified, and this time with some comments:

Configuration passages:

define('SALESFORCE_USER', 'integration@example.com.sbox1');
define('SALESFORCE_PASS', '*password here*');
define('SALESFORCE_TOKEN', '*security token here*');
define('SALESFORCE_WSDL', '/path/to/enterprise.wsdl.xml');

Code:

require_once('SforceEnterpriseClient.php');

/**
 * explicitly turn off WSDL caching
 * there is a bug in PHP with this setting in php.ini
 * http://bugs.php.net/bug.php?id=41665
 * so it's safest to set it in the PHP source
 *
 * This param should take an int, not a string like most examples use
 * see http://us3.php.net/manual/en/soap.configuration.php
*/
ini_set('soap.wsdl_cache_enabled', 0);

// instantiate a new Salesforce Enterprise object
$crmHandle = new SforceEnterpriseClient();

// instantiate a SOAP connection to Salesforce
try {
  $crmHandle->createConnection(SALESFORCE_WSDL);
} catch (Exception $e) {
  // handle exception - did you set the WSDL path above?
  // we may also be in a Salesforce outage right now
}

// log in to Salesforce
try {
  $crmHandle->login(SALESFORCE_USER, SALESFORCE_PASS . SALESFORCE_TOKEN);
} catch (Exception $e) {
  // handle exception - did you modify the credentials above to your own?
}

// create our lead
$lead = array();
$lead['FirstName'] = 'Joe';
$lead['LastName'] = 'Moke';
$lead['Email'] = 'jmoke@example.com';
$lead['Title'] = 'CEO';

// create the lead
// $lead must be wrapped in an array, as create() can create
// many objects with a single API call
$result = $crmHandle->create(array($lead), 'Lead');

But Wait, Where’s Our Lead?

In this case, there was no exception thrown, but a var_dump() of the $result object will reveal what went wrong:

  ["errors"]=>
  object(stdClass)#8 (3) {
    ["fields"]=>
    string(7) "Company"
    ["message"]=>
    string(38) "Required fields are missing: [Company]"
    ["statusCode"]=>
    string(22) "REQUIRED_FIELD_MISSING"
  }
  ["id"]=>
  NULL
  ["success"]=>
  bool(false)

Salesforce will usually throw an exception when things go haywire, but not always, as in our example. When there is an error, and no exception is thrown, $result->success will equal false, so always be prepared for both possibilities, and you can re-throw or create an exception as it applies to your situation.

The REQUIRED_FIELD_MISSING error is actually fairly unusual to see in the API. In nearly all cases, creating and updating via the API will exempt you from populating required fields, except where it would render the resulting object useless (omitting LastName from a lead, for instance). So, let’s set the company, and take another look at var_dump($result):

$lead['Company'] = 'Bob\'s Country Bunker';

object(stdClass)#7 (2) {
  ["id"]=>
  string(18) "00QS00000034zU4MAI"
  ["success"]=>
  bool(true)
}

Creating a Lead with the Partner WSDL

As promised, the complete version that is compatible with the Partner WSDL is as follows:

define('SALESFORCE_USER', 'integration@example.com.sbox1');
define('SALESFORCE_PASS', '*password here*');
define('SALESFORCE_TOKEN', '*security token here*');
define('SALESFORCE_WSDL', '/path/to/partner.wsdl.xml');

require_once('SforcePartnerClient.php');

/**
 * explicitly turn off WSDL caching
 * there is a bug in PHP with this setting in php.ini
 * http://bugs.php.net/bug.php?id=41665
 * so it's safest to set it in the PHP source
 *
 * This param should take an int, not a string like most examples use
 * see http://us3.php.net/manual/en/soap.configuration.php
*/
ini_set('soap.wsdl_cache_enabled', 0);

// instantiate a new Salesforce Partner object
$crmHandle = new SforcePartnerClient();

// instantiate a SOAP connection to Salesforce
try {
  $crmHandle->createConnection(SALESFORCE_WSDL);
} catch (Exception $e) {
  // handle exception - did you set the WSDL path above?
  // we may also be in a Salesforce outage right now
}

// log in to Salesforce
try {
  $crmHandle->login(SALESFORCE_USER, SALESFORCE_PASS . SALESFORCE_TOKEN);
} catch (Exception $e) {
  // handle exception - did you modify the credentials above to your own?
}

// create our lead
$lead = new sObject();
$lead->type = 'Lead';
$lead->fields = array('FirstName' => 'Joe',
                      'LastName' => 'Moke',
                      'Email' => 'jmoke@example.com',
                      'Title' => 'CEO',
                      'Company' => 'Bob\'s Country Bunker');

// create the lead
// $lead must be wrapped in an array, as create() can create
// many objects with a single API call
$result = $crmHandle->create(array($lead), 'Lead');

WSDL Type Mismatches

Specifying the wrong WSDL such as pointing Salesforce to the Enterprise WSDL when you’re instantiating the SalesforcePartnerClient or vice-versa can give some obscure exception messages, such as this one:

Fatal error: Uncaught SoapFault exception: [soapenv:Client] Element {}item invalid
    at this location in /var/www...

Hopefully, if you see this message, this post will jog your memory.

API Coding Style

I always use the variable $crmHandle as the handle to my CRM object, and names like $lead or $contact as references to individual objects. From now on, we will leave the error/exception handling to you, as the different approaches vary widely. As Salesforce does routinely undergo maintenance, and does on occasion throw exceptions itself (Java socket exceptions, etc…) I would strongly recommend that you try/catch essential calls (e.g. createConnection()), and that you handle these exceptions in a way that doesn’t bring down your site if they occur. Registering a global exception handler with set_exception_handler() and re-throwing exceptions as CrmException($e->getMessage) or CrmCreateException… is certainly a good start.

User Input Considerations

There are a couple of simple things you can check user-supplied data for to avoid throwing exceptions left, right, and center.

  • Validate email addresses. An extremely thorough, yet untested (by me) script can be found here. Salesforce will throw an exception if the email address is not RFC-compliant.
  • Enforce field length limits. Have a look at Setup > Customize > {Object Name} > Fields for lengths. Data exceeding field lengths will throw an exception. Salesforce will trim whitespace for you, so factor that into your length calculations.

Documentation and References

Apart from the lack of PHP code examples, you’ll find that the API Developer’s Guide is an invaluable resource throughout your Salesforce PHP development.

A handy (and extensive) reference regarding the differences in capabilities and thresholds between the different Salesforce editions can be found here. This PDF is quite useful to keep at hand in order to understand exactly the limitations of your particular instance.

Wrapping Up

I realize this was lengthy, but to paraphrase Albert Einstein, I tried to make things as simple as possible, but not simpler. Coming up, we’ll take a look at some examples involving converting leads with the API, and creating and updating Contact and Account objects in Salesforce.

[Post to Twitter] Tweet This Post 

-->