developerforce/force.com-toolkit-for-php
A PHP toolkit for integrating with Salesforce, providing wrappers for the REST, SOAP, Bulk, Metadata, and Streaming APIs plus OAuth authentication. It simplifies building Force.com/Salesforce apps and syncing data from PHP.
The toolkit is a thin, namespace-less wrapper around the Salesforce SOAP API (the REST layer is incidental). There is no Composer autoload in the 2015 release — you require the classes and point the client at a WSDL file.
composer require developerforce/force.com-toolkit-for-php
require_once 'vendor/developerforce/force.com-toolkit-for-php/soapclient/SforcePartnerClient.php';
$client = new SforcePartnerClient();
$client->createConnection(base_path('soapclient/partner.wsdl.xml'));
$client->login(config('salesforce.username'), config('salesforce.password') . config('salesforce.token'));
// first useful call: pull records into an Eloquent-friendly shape
$res = $client->query("SELECT Id, Name, BillingCity FROM Account LIMIT 200");
foreach ($res->records as $record) {
Account::updateOrCreate(['sf_id' => $record->Id], ['name' => $record->Name]);
}
Where to look first:
soapclient/partner.wsdl.xml — use the Partner WSDL, not Enterprise. It is schema-agnostic, so you can query/update any standard or custom object without regenerating WSDL when your org changes. Enterprise WSDL is only worth it if you want typed properties and IDE completion on a frozen schema.SforceBaseClient — the shared implementation of query, queryMore, create, update, upsert, delete, retrieve, describeSObjects, plus getLastRequest() / getLastResponse() (your primary debugging tool).SforceBaseClient::sObject — the generic record container. You set __c custom fields, Id, and type as public properties on it.The fastest first use case is a one-way sync: form submit → create(new SforceLead(...)), or a nightly query() of opportunities into a local reporting table. Do that before attempting bidirectional sync; the toolkit has no change-tracking or delta helpers, so you must build SystemModstamp-based watermarks yourself.
Wrap it in a singleton, not a per-request login. login() is an authenticated API call and counts against your org's login-call limit; calling it on every HTTP request will throttle you. In Laravel:
// SalesForceServiceProvider::register()
$this->app->singleton(SforcePartnerClient::class, function () {
$client = new SforcePartnerClient();
$client->createConnection(config('salesforce.wsdl'));
if ($session = Cache::get('salesforce.session')) {
$client->setSessionHeader($session['id']);
$client->setEndpoint($session['url']);
return $client; // skip login entirely
}
$client->login(config('salesforce.username'), config('salesforce.password').config('salesforce.token'));
Cache::put('salesforce.session', [
'id' => $client->getSessionId(),
'url' => $client->getLocation(),
], now()->addHours(6));
return $client;
});
Catch SoapFault with INVALID_SESSION_ID, flush the cache key, and retry once in a middleware or a rescue() wrapper.
Use a repository layer, not the client in controllers. The client mutates global session state and returns raw stdClass; keep it behind a SalesforceRepository that maps arrays ↔ sObject:
public function upsertAccount(array $data): string
{
$s = new SforceBaseClient::sObject();
$s->type = 'Account';
$s->External_Key__c = $data['external_key']; // upsert key must be an externalId field
foreach ($data as $field => $value) {
$s->{$field} = $value;
}
$result = $this->client->upsert('External_Key__c', [$s]);
return $result[0]->id;
}
Batch writes 200 at a time. create/update/upsert accept arrays and are processed as one API call. Chunk your queue jobs with array_chunk($records, 200) rather than looping single records — this is the single biggest throughput lever available in this package.
Paginate with queryMore, and always check done. query() returns at most 2,000 records per call (15,000 total for a SOQL query). Loop while !$res->done with $res->queryLocator. Never assume a single query() result is complete.
Incremental sync via SystemModstamp. Store the max SystemModstamp seen, then WHERE SystemModstamp > :ts on the next run. Because the toolkit has no Bulk API support, for anything over ~50k rows overnight, write the CSV and POST it to /services/async/.../job yourself with Guzzle — mixing the two is normal and expected.
Metadata changes go through SforceMetadataClient with a zipped package.xml + source; treat it as a deployment script, not runtime code.
php-soap is not enabled by default on many images; the client fails with an opaque "could not connect" if SoapClient is missing.$s->Field = null will not clear a field. Use $s->fieldsToNull = ['Field__c'] on a Partner sObject, and remember it replaces the array on each write.true/false (not 1/0), dates as Y-m-d, datetimes as Y-m-d\TH:i:s\Z (UTC). Picklist values are case-sensitive and must match describeSObjects() output — cache the describes, they're cheap but rate-limited.\ then ' on every interpolated string, and validate anything used as a field or object name against a whitelist.$e->faultstring and $client->getLastRequest() / getLastResponse(); the fault message alone rarely identifies the offending field. Partial-success array responses from batch writes contain per-record success and errors — check them all, never just [0].setEndpoint() trick lets you target a newer /services/Soap/u/58.0/... endpoint with the older WSDL for most CRUD, but new fields/objects won't describe correctly. Plan for a migration path (omniphx/forrest for REST, or hand-rolled REST + JWT OAuth) rather than betting on this package long-term.access_token via the JWT bearer flow yourself and call setSessionHeader($token) + setEndpoint($instanceUrl . '/services/Soap/u/58.0') to bypass login() entirely.sObject under PHP 8.2+. Add #[AllowDynamicProperties] via a subclass or silence with error_reporting. Also no security patches — pin it, vendor-review it, and isolate it behind your own interface so swapping it out is a one-class change.SforceBaseClient (override setHeaders / _getClient) and the SforceBaseClient::$sObject container. That's it — there are no events, middleware, or plugins.How can I help you explore Laravel packages today?