phpforce / soap-client Goto Github PK
View Code? Open in Web Editor NEWA PHP client for the Salesforce SOAP API
License: MIT License
A PHP client for the Salesforce SOAP API
License: MIT License
Hey, for the connections that we're making to salesforce, we need to use a different namespace. Currently, the SOAP_NAMESPACE constant holds the namespace value, which is hardcoded to 'urn:enterprise.soap.sforce.com'.
I would propose that the builder get an option to modify the namespace during composition time.
What do you think?
Hi when i try to install the package i get this error:
Problem 1 - Installation request for phpforce/soap-client ^0.1.0 -> satisfiable by phpforce/soap-client[0.1.0]. - phpforce/soap-client 0.1.0 requires phpforce/common dev-master -> satisfiable by phpforce/common[dev-master] but these conflict with your requirements or minimum-stability.
what should i do?
It would be nice if the upsert()
function would return a upsertResult
object when called. Right now it's very hard to find out what went wrong when upsert isn't working.
Is this still being maintained?
Deleted
As I've recently switched jobs, and I no longer use Salesforce, I'm looking for maintainers for this project. Are you interested in maintaining this project? Please leave a comment or send me a message.
Can you please add API version to the Readme file.
This is so we can track when the API version is being retired and decide if this client needs updating.
Thank you
Hi all,
i am having troubles with this error
UNSUPPORTED_CLIENT: TLS 1.0 has been disabled in this organization. Please use TLS 1.1 or higher when connecting to Salesforce using https.
Any ideas for a possible fix?
Thanks
Using the package with PHP 8.1 emits deprecation notices (this and few other ones):
Deprecated function: Return type of Phpforce\SoapClient\Result\RecordIterator::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in include() (line 15 of /var/www/html/vendor/phpforce/soap-client/src/Phpforce/SoapClient/Result/RecordIterator.php)
The method Client::checkResults checks the array of results returned by the Salesforce APIs and throws a SaveException when at least one of the results is not a Success. However in a catch block one can only retrieve from the exception the results that were indeed a failure but there is no way to handle those that weren't. Wouldn't it better if the SaveException had a way to get all the results of a call?
I'm not sure what's the best way here, I can only think of adding all the results in the exception no matter what (the error message of the exception is still only bound to those results that has an error).
travis.yml indicates this is compatible with HHVM. I'm not seeing that. I'm using HHVM 3.7. PHP5-FPM works just fine. With HHVM, LoginResult just comes back with a bunch of NULL values in the array of protected values, leading me ultimately to an error from Salesforce of "Destination URL not reset. The URL returned from login must be set in the SforceService" from a subsequent request.
I'm going to mess with this a bit more. Would be nice to solve. Wondering if this is fundamentally the culprit: http://docs.hhvm.com/manual/en/soapclient.soapclient.php .
Totally not the fault of phpforce, but using the soap option CACHE_MEMORY or CACHE_BOTH currently causes segfaults on the latest versions of PHP (e.g. 7.0.11)
The upstream bug is still open - it seems to require usage from a CLI context to be triggered, but is very repeatable.
I wonder if you'd consider leaving this configuration option up to the php.ini file's soap.wsdl_cache
option rather than hard-coding it in the constructor. That would allow this library to be compatible with PHP7 immediately, rather than having to wait for the upstream bug to close (and, remember, it'll never be compatible with 7.0.0 - 7.0.11 as is...)
Hi,
How do you make it work with the Partner WSDL ?
If you are trying to use this bundle for a PARTNER type wsdl, as I ultimately did, here's a way to go about it. It would be great if this bundle did this natively.
I went this route because a.) I didn't like the bloat from BeSimple and Zendframework with another client bundle, and b.) I needed Partner WSDL support, not Enterprise.
First, extend the Phpforce client in you own bundle. I chose to keep my phpforce extended and supporting classes in a subdirectory called Phpforce. Notable aspects of this extension are an override of the SOAP_NAMESPACE constant and an override of all methods that use this constant. But also of crucial importance is the alteration to the createSoapVars function, wherein the "any" field is properly processed on an SObject.
One other note is that you'll see we're not setting the username/password/apiKey in the constructor. If you're using the Partner wsdl, flexibility in credentials seems to me to be one of the main reasons. Here I'll be using an entity called SalesforceCredentials to manage those login settings. Achieve the same thing however you want.
<?php
namespace Acme\SalesforceBundle\Phpforce;
use Acme\SalesforceBundle\Entity\SalesforceCredentials;
use Phpforce\SoapClient\Soap\SoapClient;
use Phpforce\SoapClient\Client as BaseClient;
class Client extends BaseClient
{
const SOAP_NAMESPACE = 'urn:partner.soap.sforce.com';
public function __construct(SoapClient $soapClient)
{
parent::__construct($soapClient, null, null, null);
}
public function setCredentials(SalesforceCredentials $salesforce) {
$this->username = $salesforce->getUsername();
$this->password = $salesforce->getPassword();
$this->token = $salesforce->getApiKey();
}
public function login($username, $password, $token)
{
try {
$result = $this->soapClient->login(array(
'username' => $username,
'password' => $password.$token
));
} catch (\SoapFault $soapFault) {
return $soapFault;
}
$this->setLoginResult($result->result);
return $result->result;
}
public function getLoginResult()
{
if (null === $this->loginResult) {
$loginResult = $this->login($this->username, $this->password, $this->token);
if (get_class($loginResult) == 'SoapFault') {
return $loginResult;
}
}
return $this->loginResult;
}
public function merge(array $mergeRequests, $type)
{
foreach ($mergeRequests as $mergeRequest) {
if (!($mergeRequest instanceof Request\MergeRequest)) {
throw new \InvalidArgumentException(
'Each merge request must be an instance of MergeRequest'
);
}
if (!$mergeRequest->masterRecord || !is_object($mergeRequest->masterRecord)) {
throw new \InvalidArgumentException('masterRecord must be an object');
}
if (!$mergeRequest->masterRecord->Id) {
throw new \InvalidArgumentException('Id for masterRecord must be set');
}
if (!is_array($mergeRequest->recordToMergeIds)) {
throw new \InvalidArgumentException('recordToMergeIds must be an array');
}
$mergeRequest->masterRecord = new \SoapVar(
$this->createSObject($mergeRequest->masterRecord, $type),
SOAP_ENC_OBJECT,
$type,
self::SOAP_NAMESPACE
);
}
return $this->call(
'merge',
array('request' => $mergeRequests)
);
}
protected function createSoapVars(array $objects, $type)
{
$soapVars = array();
foreach ($objects as $object) {
$sObject = $this->createSObject($object, $type);
// REALLY IMPORTANT ALTERATION. (PARTNER WSDL COMPATIBILITY).
if (isset($sObject->any)) {
foreach ($sObject->any as $key => $value) {
$sObject->{$key} = $value;
}
unset($sObject->any);
}
$xml = '';
if (isset($sObject->fieldsToNull)) {
foreach ($sObject->fieldsToNull as $fieldToNull) {
$xml .= '<fieldsToNull>' . $fieldToNull . '</fieldsToNull>';
}
$fieldsToNullVar = new \SoapVar(new \SoapVar($xml, XSD_ANYXML), SOAP_ENC_ARRAY);
$sObject->fieldsToNull = $fieldsToNullVar;
}
$soapVar = new \SoapVar($sObject, SOAP_ENC_OBJECT, $type, self::SOAP_NAMESPACE);
$soapVars[] = $soapVar;
}
return $soapVars;
}
protected function setSoapHeaders(array $headers)
{
$soapHeaderObjects = array();
foreach ($headers as $key => $value) {
$soapHeaderObjects[] = new \SoapHeader(self::SOAP_NAMESPACE, $key, $value);
}
$this->soapClient->__setSoapHeaders($soapHeaderObjects);
}
protected function setSessionId($sessionId)
{
$this->sessionHeader = new \SoapHeader(
self::SOAP_NAMESPACE,
'SessionHeader',
array(
'sessionId' => $sessionId
)
);
}
}
The Partner API is going to respond to queries with an "all" key full of namespaced xml, which needs to be converted. Processing those to a proper SObject requires borrowing a class from the Salesforce PHP Toolkit.
<?php
/*
* This class taken from the Official PHP Salesforce Toolkit.
* Added preceding slash to stdClass for compatibility
* https://github.com/developerforce/Force.com-Toolkit-for-PHP/blob/master/soapclient/SforceBaseClient.php
*/
namespace Acme\SalesforceBundle\Phpforce;
class SObject {
public $type;
public $fields;
// public $sobject;
public function __construct($response=NULL) {
if (!isset($response) && !$response) {
return;
}
foreach ($response as $responseKey => $responseValue) {
if (in_array($responseKey, array('Id', 'type', 'any'))) {
continue;
}
$this->$responseKey = $responseValue;
}
if (isset($response->Id)) {
$this->Id = is_array($response->Id) ? $response->Id[0] : $response->Id;
}
if (isset($response->type)) {
$this->type = $response->type;
}
if (isset($response->any)) {
try {
//$this->fields = $this->convertFields($response->any);
// If ANY is an object, instantiate another SObject
if ($response->any instanceof stdClass) {
if ($this->isSObject($response->any)) {
$anArray = array();
$sobject = new SObject($response->any);
$anArray[] = $sobject;
$this->sobjects = $anArray;
} else {
// this is for parent to child relationships
$this->queryResult = new QueryResult($response->any);
}
} else {
// If ANY is an array
if (is_array($response->any)) {
// Loop through each and perform some action.
$anArray = array();
// Modify the foreach to have $key=>$value
// Added on 28th April 2008
foreach ($response->any as $key=>$item) {
if ($item instanceof stdClass) {
if ($this->isSObject($item)) {
$sobject = new SObject($item);
// make an associative array instead of a numeric one
$anArray[$key] = $sobject;
} else {
// this is for parent to child relationships
//$this->queryResult = new QueryResult($item);
if (!isset($this->queryResult)) {
$this->queryResult = array();
}
array_push($this->queryResult, new QueryResult($item));
}
} else {
//$this->fields = $this->convertFields($item);
if (strpos($item, 'sf:') === false) {
$currentXmlValue = sprintf('<sf:%s>%s</sf:%s>', $key, $item, $key);
} else {
$currentXmlValue = $item;
}
if (!isset($fieldsToConvert)) {
$fieldsToConvert = $currentXmlValue;
} else {
$fieldsToConvert .= $currentXmlValue;
}
}
}
if (isset($fieldsToConvert)) {
// If this line is commented, then the fields becomes a stdclass object and does not have the name variable
// In this case the foreach loop on line 252 runs successfuly
$this->fields = $this->convertFields($fieldsToConvert);
}
if (sizeof($anArray) > 0) {
// To add more variables to the the top level sobject
foreach ($anArray as $key=>$children_sobject) {
$this->fields->$key = $children_sobject;
}
//array_push($this->fields, $anArray);
// Uncommented on 28th April since all the sobjects have now been moved to the fields
//$this->sobjects = $anArray;
}
/*
$this->fields = $this->convertFields($response->any[0]);
if (isset($response->any[1]->records)) {
$anArray = array();
if ($response->any[1]->size == 1) {
$records = array (
$response->any[1]->records
);
} else {
$records = $response->any[1]->records;
}
foreach ($records as $record) {
$sobject = new SObject($record);
array_push($anArray, $sobject);
}
$this->sobjects = $anArray;
} else {
$anArray = array();
$sobject = new SObject($response->any[1]);
array_push($anArray, $sobject);
$this->sobjects = $anArray;
}
*/
} else {
$this->fields = $this->convertFields($response->any);
}
}
} catch (Exception $e) {
var_dump('exception: ', $e);
}
}
}
function __get($name) { return (isset($this->fields->$name))? $this->fields->$name : false; }
function __isset($name) { return isset($this->fields->$name); }
/**
* Parse the "any" string from an sObject. First strip out the sf: and then
* enclose string with <Object></Object>. Load the string using
* simplexml_load_string and return an array that can be traversed.
*/
function convertFields($any) {
$str = preg_replace('{sf:}', '', $any);
$array = $this->xml2array('<Object xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$str.'</Object>', 2);
$xml = new \stdClass();
if (!count($array['Object']))
return $xml;
foreach ($array['Object'] as $k=>$v) {
$xml->$k = $v;
}
//$new_string = '<Object xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$new_string.'</Object>';
//$new_string = $new_string;
//$xml = simplexml_load_string($new_string);
return $xml;
}
/**
*
* @param string $contents
* @return array
*/
function xml2array($contents, $get_attributes=1) {
if(!$contents) return array();
if(!function_exists('xml_parser_create')) {
//print "'xml_parser_create()' function not found!";
return array('not found');
}
//Get the XML parser of PHP - PHP must have this module for the parser to work
$parser = xml_parser_create();
xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, 0 );
xml_parser_set_option( $parser, XML_OPTION_SKIP_WHITE, 1 );
xml_parse_into_struct( $parser, $contents, $xml_values );
xml_parser_free( $parser );
if(!$xml_values) return;//Hmm...
//Initializations
$xml_array = array();
$parents = array();
$opened_tags = array();
$arr = array();
$current = &$xml_array;
//Go through the tags.
foreach($xml_values as $data) {
unset($attributes,$value);//Remove existing values, or there will be trouble
//This command will extract these variables into the foreach scope
// tag(string), type(string), level(int), attributes(array).
extract($data);//We could use the array by itself, but this cooler.
$result = '';
if ($get_attributes) {
switch ($get_attributes) {
case 1:
$result = array();
if(isset($value)) $result['value'] = $value;
//Set the attributes too.
if(isset($attributes)) {
foreach($attributes as $attr => $val) {
if($get_attributes == 1) $result['attr'][$attr] = $val; //Set all the attributes in a array called 'attr'
/** :TODO: should we change the key name to '_attr'? Someone may use the tagname 'attr'. Same goes for 'value' too */
}
}
break;
case 2:
$result = array();
if (isset($value)) {
$result = $value;
}
//Check for nil and ignore other attributes.
if (isset($attributes) && isset($attributes['xsi:nil']) && !strcasecmp($attributes['xsi:nil'], 'true')) {
$result = null;
}
break;
}
} elseif (isset($value)) {
$result = $value;
}
//See tag status and do the needed.
if($type == "open") {//The starting of the tag '<tag>'
$parent[$level-1] = &$current;
if(!is_array($current) or (!in_array($tag, array_keys($current)))) { //Insert New tag
$current[$tag] = $result;
$current = &$current[$tag];
} else { //There was another element with the same tag name
if(isset($current[$tag][0])) {
array_push($current[$tag], $result);
} else {
$current[$tag] = array($current[$tag],$result);
}
$last = count($current[$tag]) - 1;
$current = &$current[$tag][$last];
}
} elseif($type == "complete") { //Tags that ends in 1 line '<tag />'
//See if the key is already taken.
if(!isset($current[$tag])) { //New Key
$current[$tag] = $result;
} else { //If taken, put all things inside a list(array)
if((is_array($current[$tag]) and $get_attributes == 0)//If it is already an array...
or (isset($current[$tag][0]) and is_array($current[$tag][0]) and ($get_attributes == 1 || $get_attributes == 2))) {
array_push($current[$tag],$result); // ...push the new element into that array.
} else { //If it is not an array...
$current[$tag] = array($current[$tag],$result); //...Make it an array using using the existing value and the new value
}
}
} elseif($type == 'close') { //End of tag '</tag>'
$current = &$parent[$level-1];
}
}
return($xml_array);
}
/*
* If the stdClass has a done, we know it is a QueryResult
*/
function isQueryResult($param) {
return isset($param->done);
}
/*
* If the stdClass has a type, we know it is an SObject
*/
function isSObject($param) {
return isset($param->type);
}
}
Next turn this into a service in your service.xml. I did elect to store the wsdl locally as Acme/SalesforceBundle/Phpforce/partner.wsdl.xml, reflected here.
acme.salesforce_client:
class: Acme\SalesforceBundle\Phpforce\Client
arguments: [ @acme.salesforce.client.soap.client ]
acme.salesforce.client.soap.client:
factory_service: acme.salesforce.soap.client.factory
factory_method: factory
arguments: [ "%kernel.root_dir%/../src/Acme/SalesforceBundle/Phpforce/partner.wsdl.xml" ]
class: Phpforce\SoapClient\Soap\SoapClient
acme.salesforce.soap.client.factory:
class: Phpforce\SoapClient\Soap\SoapClientFactory
All right, so now a few important things in a controller. Login (successfully), do a query, and do an update.
//... Some containeraware controller method
//... or pass @acme.salesforce_client to another service as an argument
use Acme\SalesforceBundle\Phpforce\SObject;
$this->client = $this->container->get('acme.salesforce_client');
$this->client->setCredentials($salesforceCredentials);
$loginResult = $this->client->getLoginResult();
if (get_class($loginResult) == 'SoapFault') {
// $error_string = $loginResult->getMessage();
// add $error_string to FlashBag or whatever.
}
else {
// First will query for a Lead object with xyz email address.
// But we'll also ask for all fields, including custom, that we got when we did a describe call on the Lead.
$email = '[email protected]';
$query = 'select Id, isConverted, isDeleted';
foreach($filtered_field_array_taken_from_describe['fields'] as $field) {
$query .= ', ' . $field['name'];
}
$query .= ' from Lead where Email = \'' . $email . '\' limit 5';
$result = $this->client->query($query);
foreach ($result as $record) {
$sObject = new SObject($record);
// Now do what you will with fields, like $sObject->fields->IsConverted;
// Or what you will with $record->getId(); (which might be an array)
}
// Now an update call
// Here updating a lead, with fake $postData from wherever.
// the $Id presumably would come from the query above.
$lead = new \stdClass();
$lead->Id = $Id;
$lead->type = 'Lead';
$lead->any = new \stdClass();
$postData = array('FirstName' => 'Joe', 'LastName' => 'Smith');
foreach ($postData as $key => $value) {
$lead->any->$key = $value;
}
$response = $this->client->update(array($lead), 'sObject');
}
//...
Thanks for the great Salesforce bundles, David.
Applying the fix in #9 exposed a bug in the PHP SOAP library where SoapClient::__getTypes()
does not support extensions in the XMLSchema when used in WSDL mode.
https://bugs.php.net/bug.php?id=45404
Since every object in the WSDL extends sObject
, which is where the Id
is declared, any record update will fail with "Id not specified in an update call".
Hi
Does anyone have an example of using the create()
SOAP call?
I'm trying to create a new Account/Lead, but unsure what I need to send as part of the call
Thanks
A quick getting started guide showing how these classes relate to the official toolkit would be really helpful.
convertLead returns a LeadConvertResult array but it seems like that array is not checked using the checkResult method like other similar calls in the library? Is there any special reason to not doing so?
This might be an easy question, but I've not been able to find a definitive answer in the code (yet)
Does this support the Metadata or Tooling API's as well as the Enterprise? We need some functionality available in Metadata/Tooling's SOAP that isn't available for Enterprise (updating picklists through the API)
Thanks,
-DF
See: phpforce/common#2
Can this module get a release made which includes a stable tagged version of the common library? Relying on dev-master is making life difficult ๐
Hello,
Can you help me please?
I have problem with connection to salesforce, when i use query i have this error Element {}item invalid at this location
vendor\phpforce\soap-client\src\Phpforce\SoapClient\Client.php (line 560)
In method
$requestEvent = new Event\RequestEvent($method, $params); $this->dispatch(Events::REQUEST, $requestEvent); try { $result = $this->soapClient->$method($params); } catch (\SoapFault $soapFault) { $faultEvent = new Event\FaultEvent($soapFault, $requestEvent); $this->dispatch(Events::FAULT, $faultEvent); throw $soapFault;
my code
class Salesforce
{
const USERNAME_SF = "username";
const TOKEN_SF = "token";
const PASSWORD_SF = "pass";
private $client;
private function login()
{
if(!is_null($this->client)) return;
// check if l'utilisateur existe mot de pass dynamique
$auth= new \Phpforce\SoapClient\ClientBuilder(
__DIR__ . '/../../config/wsdl/PartnerWSDL.xml',
self::USERNAME_SF,
self::PASSWORD_SF,
self::TOKEN_SF
);
$this->client = $auth->build();
}
public function get_information()
{
$this->login();
$results = $this->client->query('select Id from Contact');
}
}
Thanks
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.