PHP 5's SOAP extension and SalesForce

In the last few days I've been working on a piece of code in PHP that had to integrate with SalesForce. During the course of this work I bumped into some minor annoyances (or rather, a minor annoyance) with ext/soap, which I was able to quickly fix thanks to Dmitry's excellently structured code. For those who are interested, you can now set custom SOAP headers using SoapClient::__setSoapHeaders(). This will cause SoapClient to send the headers on every subsequent access to a web service, conducted through the relevant SoapClient object. While you could specify custom SOAP headers before - it required overloading __call() and doing this work in userland, and now it's no longer necessary.

On a slightly related note, I wanted to take this opportunity to ask how many of you have actually used ext/soap, and what's your feedback about it. Is it working well? Is there anything missing? As in the past, success stories are also interesting, they're equally interesting to "it's broken!" responses.

As a special bonus, for those of you who integrate with SalesForce, and are still using the old SOAP APIs (or God forbid, are using a language other than PHP to do it), here's a code snippet that demos how to connect to SalesForce. Note that because it uses the new SoapClient::__setSoapHeaders(), you'd need a fairly recent snapshot/CVS in order for that to work.

---
PHP:

<?php
// Connect
$sforce = new SoapClient("sforce.wsdl");  // obtain your own customized sforce.wsdl from sforce.com
$loginResult $sforce->login(array("username" => "joe@doe.com""password" => "xyz"));
$loginResult$loginResult->result;

// Set connection data
$sforce->__setLocation($loginResult->serverUrl);
$sforce_header = new SoapHeader('urn:enterprise.soap.sforce.com''SessionHeader', array('sessionId' => $loginResult->sessionId));
$sforce->__setSoapHeaders(array($sforce_header));

// Query SalesForce for Leads whose first name is Bill
try {
    
$result $sforce->query(array("queryString" => "select Id, FirstName, LastName from Lead where Lead.FirstName='Bill'"));
    
print_r($result);
} catch (
Exception $e) {
    
print_r($e);
}

// Change a lead
try {
    
$lead->LastName "Gates";
    
$lead->Id "...";  // needs to be a valid id
    
$arg->sObjects = new SoapVar($leadSOAP_ENC_OBJECT"Lead""http://soapinterop.org/xsd");
    
print_r($sforce->update($arg));
    print 
"SUCCESS!";
} catch (
Exception $e) {
    
print_r($e);
}

?>


---

During the course of writing this blog I came across a blog post from George Schlossnagle which may also be interesting. Even though I didn't bump into the same issue, it may be that I just didn't get far enough - so you may still need the workaround in there.

Trackbacks

    No Trackbacks

Comments

Display comments as (Linear | Threaded)

  1. David says:

    I have used ext/soap for quite some time now and I'm very happy with it, since it's much much faster than NuSoap or Pear soap. When I get the time I hope to extend it with support for encryption / signature (using the xmlsec lib) and attachments. Adding the classmap option with the help of Dmitry was a good start to get to know the internals of ext/soap, but my knowledge still needs to improve (besides the time factor :-)).

  2. Wez Furlong says:

    We've been integrating against SalesForce here at OmniTI. George blogged about some of our experiences:
    http://www.schlossnagle.org/~george/blog/index.php?/archives/235-Salesforce.com-and-PHP.html

  3. Zeev says:

    Yeah I know, I even linked to it :-)

  4. Wez Furlong says:

    Heh, I need my eyes checked.

  5. George Schlossnagle says:

    Yeah... as Wez mentioned we did this a while back (like you, we're Salesforce customers). Instead to taking the tack you did, I ended up extending SoapClient with a class that handled the header generation. This worked out well for us as some of the operation (particularly the update operations) require some type munging due to the way the Salesforce wsdl is constructed (basically the problem is that you need to be able to upgrade objects from an abstract base type, which is hard and completely non-transparent in PHP).

  6. Zeev says:

    Can you give me a piece of code that would demonstrate that problem? I want to know whether I'm about to meet a bumpy road ahead or not...

  7. George Schlossnagle says:

    I ran into a problem with using the create() and update(). The problem is that they are prototyped to take sObjects (an abstract schema type, which isn't well understood by PHP). I ended up doing a two part conversion. First I created the sObject descendent like this:

    $this->event = new SoapVar($this->_event, SOAP_ENC_OBJECT, "Event", "urn:sobject.enterprise.soap.sforce.com", "Event", "urn:sobject.enterprise.soap.sforce.com");

    and then I did markup on this object in a __doRequest():

    function __doRequest($request, $location, $action, $version) {
    if(!$this->currentCall || !$this->currentArgs) {
    return parent::__doRequest($request, $location, $action, $version);
    }
    $dom = new DOMDocument('1.0');
    $dom->loadXML($request);

    foreach( $dom->getElementsByTagNameNS('http://schemas.xmlsoap.org/soap/envelope/','Envelope') as $body) {
    $body->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ens", "urn:sobject.enterprise.soap.sforce.com");
    $body->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xsi", 'http://www.w3.org/2001/XMLSchema-instance');
    }
    foreach($this->currentArgs as $arg) {
    if(isset($arg->sObjects) && is_a($arg->sObjects, 'SoapVar')) {
    $prefix = $dom->lookupPrefix($arg->sObjects->enc_namens);
    foreach($dom->getElementsByTagNameNS('urn:enterprise.soap.sforce.com','sObjects') as $node) {
    $node->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'type', "$prefix:{$arg->sObjects->enc_stype}");
    }
    }
    }
    $request = $dom->SaveXML();
    return parent::__doRequest($request, $location, $action, $version);
    }

    I'm certainly open to improvements on that technique - I'll be the first to label it as hackish (though it is a nice demo of what you can do with __doRequest() to work around weird interop problems).

  8. Manfred says:

    You may also want to try a related PEAR package.
    http://pear.php.net/package/Services_Webservice

    Regards
    Manfred

  9. George Schlossnagle says:

    Isn't that for creating webservices? We're talking about consuming a specific one, the salesforce.com API.

  10. Manfred says:

    That`s why I created a new topic and did not follow your thread! Anyway, the post does not especially seem to focus on the salesforce api to me.
    Regards
    Manfred

  11. Manfred says:

    ok, to not confuse readers my post has nothing to do with the SalesForce Api.
    Regards
    Manfred

  12. aljosa says:

    i haven't done any "production code" work with ext/soap but it seems to me that some things from Services_Webservice should be in ext/soap (like wsdl generation). any comment on that?

  13. Danne says:

    I've used ext/soap for integration between legacy systems that require weird C apis on true64 and WebMethods for more than six months now. It is working really well. We have around ten thousand business critical transactions through ext/soap each day now.

  14. Anonymous says:

    is setting custom soap header using SoapClient setSoapHeaders
    going to be in the next release of php 5.0.5?

  15. Zeev says:

    Yep.

  16. Jim says:

    anyone tried php5.1beta with salesforce?
    is it better than 5.0 considering the statement below

    "Many more improvements including lots of new functionality & many bug fixes, especially in regards to SOAP..."

  17. George Schlossnagle says:

    5.0.4 works fine with Salesforce. You need to use a slightly different tact in setting up your classes - you have to use soapCall instead of the overloader to specify headers. Of course you can just extend SoapClient and use your own __call() which internall call soapCall() with the correct headers. :-)

  18. Michael Markham says:

    This is GREAT news! I've been using the older method (to overload "__call") and this new technique will be fantastic. I'm currently using PHP 5.0.4 from March 31. Any idea when "__setSoapHeaders" will make it into a GA release?

    Thanks very much for sharing this information, Zeev -- I've really enjoyed building enhancements to our salesforce.com environment using PHP. I've found using PHP5 to be an excellent and easy way to come up with custom functionality. Hope to see you at Dreamforce.

  19. Anonymous says:

    i hope 5.0.5 comes out soon =)
    its been 4 months already

  20. Marcus Bointon says:

    I just checked in the PHP 5.0.5RC1 source and __setSoapHeaders IS available in there, so this will be out in a release very soon.

  21. Hugo Samayoa says:

    Just tried 5.0.5 and the example worked without any changes. Thanks a bunch.

  22. Marcus Bointon says:

    Great news, but just my luck - I had to work around this exact same problem about a month ago! The big downer about overloading __call is that you have to futz about to make it still work WSDL style. This change to ext/soap will fix this very nicely - I had written a userland 'addHeaders' function that does the same thing, just like Wez did. Any chance you could wrap up what you've done and give it to sforce.sourceforge.net? Their current example code is fairly rough and all PHP4.

    The other thing I've had real trouble with in the SF API that may be related to PHP's implementation; The SF API has secondary functions that don't seem to be available to PHP, but they work just fine in Java. For example, a QueryResult from a 'query' command contains functions:

    queryResult = binding.query("select FirstName, LastName from Contact");
    // Determine whether the query returned all the possible records
    if (queryResult.isDone()) {
    // Iterate through the records and process them
    for (int i = 0; i < queryResult.getRecords().length; i++) {

    you can see here that the queryResult object has 'isDone' and 'getRecords' functions and it's quite elegant. I can't see any way of calling these from PHP as they are not part of the WSDL, so I'm having to examine the data with normal array functions. I guess they could map to SPL iterators or similar, but I've not got into that yet. I could be barking up the wrong tree of course.

    One other thing - it would be very helpful to be able to specify a local file or a string for the WSDL passed to a SoapClient constructor - SF's WSDL files are not available directly and they can be account-specific, so I'm quite likely to hold mine in a DB.

  23. Marcus Bointon says:

    I just got this working with a 5.1 snapshot from today (it's not available in RC1) with one tweak. this line won't work:

    $sforce_header = new SoapHeader('urn:enterprise.soap.sforce.com', 'SessionHeader', array('sessionId' => $loginResult->sessionId));

    As George's post said for his pre-5.1 example, you still need to cast the header info to an object:

    $sforce_header = new SoapHeader('urn:enterprise.soap.sforce.com', 'SessionHeader', (object)array('sessionId' => $loginResult->sessionId));

    Otherwise it just won't be a usable header.

    Can this be fixed so that it just works? I guess forcing coercion to an object would be bad if you really did want to give it an array.

  24. Zeev says:

    This appears to work fine for me (without the casting). Are you getting some error? Do you see a difference in __getLastRequest()?

  25. Marcus Bointon says:

    Without the object cast, whatever request I make, SalesForce rejects it with an INVALID_SESSION_ID error. There is a difference in getLastRequest (I'm using trace so it's easy to see this with a var_dump): This is what the request looks like without the cast:


    sessionIdzo1j2px6ZTpewZtz6Pmoy_k8USTyP9ttpqn4vU5ebncanXlSUj_dS.nhltY2PkgSRR6Cplg4Hk0Karqi.UuQJRhD2wYHcrlK4SNdGN37zi4=select Id, Name from UserRole

    and here it is with the cast:


    OOuY8cKlZlMu.082rB86u3Qco7R_xxiCndrWMafpCw7ls5qUC_0AF_R9ru9_Exi9Opz3H7gK_cTxwZY6PRaBShhD2wYHcrlK4SNdGN37zi4=select Id, Name from UserRole

    You can see that the array version puts and wrappers around the session ID.

  26. Marcus Bointon says:

    Hm. Looks like your blog munged that, so here they are again with line breaks:


    sessionId
    zo1j2px6ZTpewZtz6Pmoy_k8USTyP9ttpqn4vU5ebncanXlSUj_dS.nhltY2PkgSRR6Cplg4Hk0Karqi.UuQJRhD2wYHcrlK4SNdGN37zi4=
    select Id, Name from
    UserRole


    OOuY8cKlZlMu.
    082rB86u3Qco7R_xxiCndrWMafpCw7ls5qUC_0AF_R9ru9_Exi9Opz3H7gK_cTxwZY6PRaBShhD2wYHcrlK4SNdGN37zi4=
    select Id, Name from
    UserRole

  27. Marcus Bointon says:

    Take 3, this time with encoded entities (seems your blog can't decide whether to interpret stuff as HTML or text - preview looked fine but posting did not!):


    &lt;?xml version="1.0" encoding="UTF-8"?&gt;
    &lt;SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:ns1="urn:enterprise.soap.sforce.com"
    xmlns:ns2="enterprise.soap.sforce.com"&gt;&lt;SOAP-ENV:Header&gt;&lt;ns2:SessionHeader&gt;&lt;item&gt;&lt;key&gt;sessionId&lt;/key&gt;&lt;value&gt;
    zo1j2px6ZTpewZtz6Pmoy_k8USTyP9ttpqn4vU5ebncanXlSUj_dS.nhltY2PkgSRR6Cplg4Hk0Karqi.UuQJRhD2wYHcrlK4SNdGN37zi4=&lt;/value&gt;
    &lt;/item&gt;&lt;/ns2:SessionHeader&gt;&lt;/SOAP-ENV:Header&gt;&lt;SOAP-ENV:Body&gt;&lt;ns1:query&gt;&lt;ns1:queryString&gt;select Id, Name from
    UserRole&lt;/ns1:queryString&gt;&lt;/ns1:query&gt;&lt;/SOAP-ENV:Body&gt;&lt;/SOAP-ENV:Envelope&gt;



    &lt;?xml version="1.0" encoding="UTF-8"?&gt;
    &lt;SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:ns1="urn:enterprise.soap.sforce.com"
    xmlns:ns2="enterprise.soap.sforce.com"&gt;&lt;SOAP-ENV:Header&gt;&lt;ns2:SessionHeader&gt;&lt;sessionId&gt;OOuY8cKlZlMu.
    082rB86u3Qco7R_xxiCndrWMafpCw7ls5qUC_0AF_R9ru9_Exi9Opz3H7gK_cTxwZY6PRaBShhD2wYHcrlK4SNdGN37zi4=&lt;/sessionId&gt;
    &lt;/ns2:SessionHeader&gt;&lt;/SOAP-ENV:Header&gt;&lt;SOAP-ENV:Body&gt;&lt;ns1:query&gt;&lt;ns1:queryString&gt;select Id, Name from
    UserRole&lt;/ns1:queryString&gt;&lt;/ns1:query&gt;&lt;/SOAP-ENV:Body&gt;&lt;/SOAP-ENV:Envelope&gt;

  28. Marcus Bointon says:

    As a kind of follow-up to this, I started this topic on the salesforce forums: http://forums.sforce.com/sforce/board/message?board.id=PerlDevelopment&message.id=644

    Considerable extra magic can be had if a submitted sObject uses the urn:sobject.enterprise.soap.sforce.com namespace instead of the generic xsd one. PHP picks up the correct item definition from the WSDL, assigns the correct namespace to the set fields and fills in unset fields when you submit an object, say as part of a create call. All a big bonus. However, it looks like there's a bug in it that is covered by that thread (no namespace assigned to nil items, incorrect value for nil parameters). If you agree, I'll write it up as a PHP bug.


The author does not allow comments to this entry