I have corrected this code to handle the one to many relationship on outbound messages from Salesforce. I will post a generic version of the code sometime tomorrow
When I first started working with outbound messaging in Salesforce, I was ecstatic that we could perform various actions based on a work flow rule, such as if a field was updated to a certain value (example: Won) on a custom object, we were able to create data in a new custom object that allows us track Design Win's.
Salesforce assigns an "ID" to each outbound message, to each Id of the object that is being used to send out the outbound message. I always thought that it was a 1 to 1 relationship, as I stated in an earlier post. This is not the case. There is a one to many relationship, which now has to be taken into consideration when writing the code to process your data coming in from Salesforce.
The following where the problem is, in the function PARSENOTIFICATION:
The function above only allows for one record to process, not many! so if you saw the incoming example message, you will see what I mean by the 1 to many relationship.PHP Code:function parseNotification($domDoc)
{
// Parse Notification parameters into result array
$result = array("OrganizationId" => "","ActionId" => "","SessionId" => "","EnterpriseUrl" => "","PartnerUrl" => "","sObject" => null);
$result["OrganizationId"] = $domDoc->getElementsByTagName("OrganizationId")->item(0)->textContent;
$result["ActionId"] = $domDoc->getElementsByTagName("ActionId")->item(0)->textContent;
$result["SessionId"] = $domDoc->getElementsByTagName("SessionId")->item(0)->textContent;
$result["EnterpriseUrl"] = $domDoc->getElementsByTagName("EnterpriseUrl")->item(0)->textContent;
$result["PartnerUrl"] = $domDoc->getElementsByTagName("PartnerUrl")->item(0)->textContent;
// Create sObject and fill fields provided in notification
$sObjectNode = $domDoc->getElementsByTagName("sObject")->item(0);
$sObjType = $sObjectNode->getAttribute("type");
if (substr_count($sObjType,"sf:"))
$sObjType = substr($sObjType,3);
$result["sObject"] = new SObject($sObjType);
$result["sObject"]->type = $sObjType;
$sObjectNodes = $domDoc->getElementsByTagNameNS('urn:sobject.enterprise.soap.sforce.com','*');
$result["sObject"]->fieldnames = array();
foreach ($sObjectNodes as $node)
{
if ($node->localName == "Id")
{ // Id goes under sObject->Id
$result["sObject"]->Id = $node->textContent;
}
else
{ // ... the rest go under sObject->fields
$fieldname = $node->localName;
$result["sObject"]->fields->$fieldname = $node->nodeValue;
array_push($result["sObject"]->fieldnames,$fieldname);
}
}
return $result;
} // end parseNotification
I rewrote the function to process all of the incoming records and loop through to get all the data.
Here is the rewritten function:
So you would get the data back with use like this:PHP Code:function parseNotification($domDoc)
{
// Parse Notification parameters into result array
$result = array("OrganizationId" => "","ActionId" => "","SessionId" => "","EnterpriseUrl" => "","PartnerUrl" => "","sObject" => null,"MapsRecords" => array());
$result["OrganizationId"] = $domDoc->getElementsByTagName("OrganizationId")->item(0)->textContent;
$result["ActionId"] = $domDoc->getElementsByTagName("ActionId")->item(0)->textContent;
$result["SessionId"] = $domDoc->getElementsByTagName("SessionId")->item(0)->textContent;
$result["EnterpriseUrl"] = $domDoc->getElementsByTagName("EnterpriseUrl")->item(0)->textContent;
$result["PartnerUrl"] = $domDoc->getElementsByTagName("PartnerUrl")->item(0)->textContent;
// Create sObject and fill fields provided in notification
$sObjectNode = $domDoc->getElementsByTagName("sObject")->item(0);
$sObjType = $sObjectNode->getAttribute("type");
if (substr_count($sObjType,"sf:"))
{
$sObjType = substr($sObjType,3);
}
$result["sObject"] = new SObject($sObjType);
$result["sObject"]->type = $sObjType;
$sObjectNodes = $domDoc->getElementsByTagNameNS('urn:sobject.enterprise.soap.sforce.com','*');
$result["sObject"]->fieldnames = array();
$count = 0;
$tempMapRecord = array();
foreach ($sObjectNodes as $node)
{
if ($node->localName == "Id")
{
if ($count > 0)
{
$result["MapsRecords"][] = $tempMapRecord;
$tempMapRecord = array();
}
$tempMapRecord[$node->localName] = $node->textContent;
}
else
{
$tempMapRecord[$node->localName] = $node->textContent;
}
$count++;
}
// Finish last item
$result["MapsRecords"][] = $tempMapRecord;
return $result;
} // end parseNotification
When we create our Design Win custom object based on the outbound SOAP message, we do the following inside the foreach() loop:PHP Code:$resultArray = parseNotification($dom);
foreach ($resultArray['MapsRecords'] as $dwin_data)
{
//now you have an array as $dwin_data and you can use the
//data as you need to for updates or calls back to salesforce
//whatever you need to do is here
}
- Query the Opportunity Object to get information needed for the Custom Object
- Query the Account Object to get information needed for the Custom Object
- Query the PriceBookEntry Object to get information needed for the Custom Object
- Query the User Object to get information needed for the Custom Object
- Build an array with all the required data
- perform a CREATE back to Salesforce to insert the new data into a new record within our custom object.
- Add a link back to the OpportunityLineItem for the new record that was created on the Design Win Object
It works perfectly!! Try the new function out and let me know if you have any issues with it.
~Mike
Hi. Apologies in advance for my newbieness. And thank you so much for this thread.. this is exactly what I'm trying to setup for my company. But i'm having trouble putting all the pieces together (and i know nothing about soap). So i'm hoping for a more complete picture of how to setup the files on my server.
I'm pretty sure my Outbound Messages are setup correctly within SF:
- i setup an Outbound Message, specified an Endpoint URL, and downloaded the wsdl.
- i setup a Workflow Rule, which finds something to send, which i can see in the queue (Administration Setup > Monitoring > Outbound Messages).
But i know i don't have things setup correctly on my server yet, so the attempted send fails.
The Queue shows:
Delivery Failure Reason "org.xml.sax.SAXException: Bad envelope tag: br".
And an error_log file is created on my server which says:
Warning: DOMDocument::loadXML() [domdocument.loadxml]: Empty string supplied as input in /home/xxx/html/dev/sf/outboundmsg/index.php on line 16
Fatal error: Call to a member function getAttribute() on a non-object in /home/xxx/html/dev/sf/outboundmsg/index.php on line 61
i'm unclear how to set things up on my server:
- i copied php code from this thread but don't understand how it all goes together in the file at my endpoint url. Any chance you could provide a complete php file?
- i replaced the parseNotification function with the update, but i'm unclear where to put the $resultArray = parseNotification($dom); foreach .
- where do i put the wsdl.xml file that i downloaded from SF?
- how/where do i point to the wsdl? Is there a config var somewhere that i missed?
- what to do with the Binding section (commented) in the wsdl from SF.
- any other editing/config of the php or wsdl that i need to do? Or additional code or files that i'm missing?
Also, one of the thread messages gave some php that included this:
/* Handles SFDC Outbound Message notification.*/
require_once ('../includes/salesforce_login.php');
but i'm not clear what that's for, or what exactly goes in that file.
I tried to figure it out from the SF help docs, but still couldn't find a simple straight-forward guide. Any help would be GREATLY appreciated.
Thanks in advance,
Todd
Todd I am going to try and break this post down to see if I can answer it in parts
So there may be multiple posts to answer all your questions and I will try and help you as much as I can
First have you tried to create a script that just performs a log in to Salesforce to make sure it works?
When you download the phptoolkit, there is really only one relevant folder that is needed. the "/soapclient" folder
all the other folders are just tutorials and such.
when I develop apps, I always create an "/includes" folder for all my common scripts like the soapclient and that login script that you said you did not know what it was
The login script is a small script that was written so I can just include that and it takes care of my login so all I have to do is a
Here is that script, so you can use itPHP Code:require_once('./includes/salesforce_login_script.php')
You need to make sure you are getting a connection to salesforce and that you can loginPHP Code:<?php
// change these paths to your fully qualified paths for each script to include
require_once ('/users/msimonds/public_html/includes/soapclient/SforcePartnerClient.php');
require_once ('/users/msimonds/public_html/includes/soapclient/SforceHeaderOptions.php');
// Login to salesforce.com
$login = "username";
$password = "password";
// change this paths to your fully qualified paths for the script to include
$wsdl = "/users/msimonds/public_html/includes/soapclient/partner.wsdl.xml";
$client = new SforcePartnerClient();
$client->createConnection($wsdl);
try
{
$loginResult = $client->login($login, $password);
if ($loginResult->passwordExpired)
{
$client = null;
}
}
catch (exception $e)
{
$client = null;
}
?>
Hope that helps, once you get that part done, I will continue to help you get this setup
~Mike
Wow, thanks Mike.. tremendous help so far.
I now have SF triggering an OutboundMsg, which is received at my EndpointURL (index.php), which writes some xml to "soap.xml".
Here's my server so far:
/dev/sf/outboundmsg/index.php (code from this thread)
/dev/sf/outboundmsg/my_wsdl.xml (from my SF OutboundMsg setup)
/dev/sf/outboundmsg/includes/soapclient/ (from phptoolkit-11_0b)
/dev/sf/outboundmsg/includes/salesforce_login_script.php (with my paths and login)
I'm ready for the next step!
I assume i need to edit index.php?
And do something with my custom WSDL from SF?
Thanks again. This is very exciting.
Todd
The following code could replace your index.php and be used to see if you are at least getting the data in an email
look at this part of the code and make sure you change the email address that I have clearly marked in the comments
this should be the next stepPHP Code:<?php
error_reporting(0);
$data = fopen('php://input','rb');
$content = stream_get_contents($data);
if ($content)
{
respond('true');
}
else
{
respond('false');
}
//login script to salesforce for the $client variable ***change this path
require_once ('/users/msimonds/public_html/includes/sfdc.inc');
// setup an array for the data coming in from the SOAP message
$dwin_data = array();
$dom = new DOMDocument();
$dom->loadXML($content);
$resultArray = parseNotification($dom);
unset($resultArray['sObject']);
// Data from SOAP message.
/***************************************************
* This is where you do all your coding *
* and process the incomingin record as *
* need. use the code: *
* $temp = '<pre>' . print_r($var,true) . '</pre>'; *
* mail('youremail@address.com', 'Suject', $temp); *
* *
* To test to make sure you are getting the *
* data in an email, change the email address *
****************************************************/
foreach ($resultArray['MapsRecords'] as $dwin_data)
{
//make sure you are getting the email with the data in
//a print_r and then we can go from there
$temp = '<pre>' . print_r($var,true) . '</pre>';
mail('youremail@address.com', 'Suject', $temp);
}
//clean all the data and then exit
unset($client);
unset($dwin_data);
unset($dwin_create);
unset($records);
exit;
//functions
/* Parse a Salesforce.com Outbound Message notification SOAP packet
* into an array of notification parms and an sObject. */
function parseNotification($domDoc)
{
// Parse Notification parameters into result array
$result = array("OrganizationId" => "","ActionId" => "","SessionId" => "","EnterpriseUrl" => "","PartnerUrl" => "","sObject" => null,"MapsRecords" => array());
$result["OrganizationId"] = $domDoc->getElementsByTagName("OrganizationId")->item(0)->textContent;
$result["ActionId"] = $domDoc->getElementsByTagName("ActionId")->item(0)->textContent;
$result["SessionId"] = $domDoc->getElementsByTagName("SessionId")->item(0)->textContent;
$result["EnterpriseUrl"] = $domDoc->getElementsByTagName("EnterpriseUrl")->item(0)->textContent;
$result["PartnerUrl"] = $domDoc->getElementsByTagName("PartnerUrl")->item(0)->textContent;
// Create sObject and fill fields provided in notification
$sObjectNode = $domDoc->getElementsByTagName("sObject")->item(0);
$sObjType = $sObjectNode->getAttribute("type");
if (substr_count($sObjType,"sf:"))
{
$sObjType = substr($sObjType,3);
}
$result["sObject"] = new SObject($sObjType);
$result["sObject"]->type = $sObjType;
$sObjectNodes = $domDoc->getElementsByTagNameNS('urn:sobject.enterprise.soap.sforce.com','*');
$result["sObject"]->fieldnames = array();
$count = 0;
$tempMapRecord = array();
foreach ($sObjectNodes as $node)
{
if ($node->localName == "Id")
{
if ($count > 0)
{
$result["MapsRecords"][] = $tempMapRecord;
$tempMapRecord = array();
}
$tempMapRecord[$node->localName] = $node->textContent;
}
else
{
$tempMapRecord[$node->localName] = $node->textContent;
}
$count++;
}
// Finish last item
$result["MapsRecords"][] = $tempMapRecord;
return $result;
} // end parseNotification
function respond($tf)
{
print '<?xml version = "1.0" encoding = "utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<notifications xmlns="http://soap.sforce.com/2005/09/outbound">
<Ack>'.$tf.'</Ack>
</notifications>
</soapenv:Body>
</soapenv:Envelope>';
}
//this is the funciton to query salesforce objects to gather data
//from different fields coming in from the SOAP message
function get_records(&$connection,&$query)
{
$queryOptions = new QueryOptions(2000);
$response = new QueryResult($connection->query($query));
// if the size is zero, where done
if ($response->size > 0)
{
$products = $response->records;
// Cycles through additional responses if the number of records
// exceeds the batch size
while (!$response->done)
{
set_time_limit(100);
$response = $connection->queryMore($response->queryLocator);
$products = array_merge($products,$response->records);
}
}
return $products;
}
?>
~Mike
Hi Mike, thanks for the continued help here.
I am getting the email now, but it does not have the soap data yet. The email only shows:
I'm not clear how $var is defined in..Code:<pre></pre>
Do i need to change something in there?Code:foreach ($resultArray['MapsRecords'] as $dwin_data) { $temp = '<pre>' . print_r($var,true) . '</pre>'; mail('my@email.com', 'soap from sf', $temp); }
And to verify that my OutboundMessage does contain data, i set index.php to log to a local file (soap.xml) again (instead of sending an email), and i still get the full soap xml, including fields and values data.
sorry if i'm missing something here. thanks in advance.
todd
Crap! My Bad
PHP Code:foreach ($resultArray['MapsRecords'] as $dwin_data)
{
//make sure you are getting the email with the data in
//a print_r and then we can go from there
$temp = '<pre>' . print_r($var,true) . '</pre>';
mail('youremail@address.com', 'Suject', $temp);
}
SHOULD BE
PHP Code:foreach ($resultArray['MapsRecords'] as $dwin_data)
{
//make sure you are getting the email with the data in
//a print_r and then we can go from there
$temp = '<pre>' . print_r($dwin_data,true) . '</pre>';
mail('youremail@address.com', 'Suject', $temp);
}
that way the $resultArray becomes the $dwin_data array in the foreach loop and you should get an email that has each element in the array with its value
Sorry about that and let me know if it works
~Mike
BINGO!
The email message contained the array and all its key => values.
I also had it just write the same thing to a local log file.
I suppose now i can get to any element of the array, like this..
$dwin_data['MySFfieldName']
and plug them into a database or something.
But of course i defer to you.. what do you do next?
And, what about the custom WSDL file (downloaded from my SF OutboundMsg setup)? That's not even used yet, right?
thanks..
todd
If you talking about the custom WSDL file that you downloaded from that outbound message, honestly you do not really need it unless you were going to access that via some APEX code or connect using PHP to that WSDL
That just gives you a list of classes or functions you can use and data access points.
Since you have the data now in an array, $dwin_data. You can do almost whatever you want to with it
- Stick it in a database
- Send it back to Salesforce into a custom Object
- Email it to someone
- Stick it in a CSV
I mean it is up to you
ALSO
I only did the initial logging to a file to make sure that I was getting the messages.
You can turn that part off
Make sure that your outbound message is getting cleared out of the queue in Salesforce. I think it is Setup > Monitoring > Outbound message queue or something like that.
Let me know if you have any other questions and I am glad that you got it to work
~Mike
Bookmarks