davispeixoto/laravel5-salesforce
Laravel 5 Salesforce integrates Salesforce REST API access into your Laravel app, giving you a simple way to authenticate and query, create, update, or delete Salesforce records through an Eloquent-like interface.
This package is a thin Laravel service-provider/facade wrapper around the (legacy) Salesforce Force.com PHP Toolkit, so everything you do is SOAP against the Enterprise or Partner WSDL. Get the connection working end-to-end before writing any domain code.
Minimal setup
Install and pin (the package targets Laravel 5.x; check the illuminate/* constraint before dropping it into a modern app):
composer require davispeixoto/laravel5-salesforce
Register the provider and alias (config/app.php) on Laravel ≤ 5.4; 5.5+ auto-discovers but the alias still needs to be added if you want the facade short name:
'providers' => [
Davispeixoto\Laravel5Salesforce\SalesforceServiceProvider::class,
],
'aliases' => [
'Salesforce' => Davispeixoto\Laravel5Salesforce\Facades\Salesforce::class,
],
Publish the config and drop your WSDL somewhere stable (do not commit a production WSDL with secrets baked into endpoints — it's org-specific for Enterprise):
php artisan vendor:publish --provider="Davispeixoto\Laravel5Salesforce\SalesforceServiceProvider"
[email protected]
SALESFORCE_PASSWORD=secret
SALESFORCE_TOKEN=securityTokenFromSetup
SALESFORCE_WSDL=/var/www/app/resources/salesforce/enterprise.wsdl
First use case — authenticate and read one object. This is the smoke test that proves credentials, token, IP allow-listing and WSDL path are all correct:
use Davispeixoto\Laravel5Salesforce\Facades\Salesforce;
Salesforce::login();
$result = Salesforce::query(
"SELECT Id, Name, BillingCity FROM Account LIMIT 5"
);
foreach ($result->records as $account) {
dump($account->Id, $account->Name);
}
Where to look first
config/salesforce.php after publishing — confirm the exact key names (username, password, token, wsdl, login URL) since they vary between releases of this wrapper.register()/singleton() binding — that tells you what class the facade resolves to and whether it logs in eagerly.vendor/davispeixoto/.../lib — the wrapper exposes very little, so the real API surface (query, queryMore, create, update, upsert, delete, describeSObjects, retrieve) lives in SforceEnterpriseClient / SforcePartnerClient.Choose your WSDL deliberately. Enterprise WSDL is generated per-org and gives typed SObjects but must be regenerated whenever you add fields; Partner WSDL is generic and stable but everything comes back as stdClass with fieldsToNull semantics. Most day-to-day code is easier against Partner, unless you're doing heavy typed mapping.
Wrap it, don't sprinkle the facade. Keep the facade out of controllers and models. Bind a repository or a small client service and inject it:
class SalesforceAccountRepository
{
public function __construct(private \SforcePartnerClient $client) {}
public function find(string $id): ?object
{
$r = $this->client->retrieve('Id, Name, BillingCity', 'Account', $id);
return $r[0] ?? null;
}
}
Then in a service provider, bind your interface to the Salesforce-backed implementation and swap it for an in-memory fake in tests.
Login once per request, not per call. The toolkit stores the session id on the client instance, and the service provider binds a singleton — so call Salesforce::login() in a middleware or a base service, and let the singleton carry the session. Re-authenticating on every method is the most common cause of hitting login rate limits.
Pagination is manual. SOQL returns at most ~2,000 records per batch; anything past that requires queryMore on the queryLocator:
$response = Salesforce::query("SELECT Id, Name FROM Contact WHERE LastModifiedDate > 2024-01-01T00:00:00Z");
while (true) {
foreach ($response->records as $record) {
// handle
}
if ($response->done) break;
$response = Salesforce::queryMore($response->queryLocator);
}
Always key sync jobs off LastModifiedDate (and SystemModstamp for replication) rather than polling everything.
Batch CRUD with SaveResult inspection. create/update/delete accept arrays and return one result per record, and Salesforce does not roll back the whole batch on a single failure:
$results = Salesforce::create($sobjects); // array of stdClass
foreach ($results as $i => $result) {
if (!$result->success) {
Log::warning('SF create failed', [
'index' => $i,
'errors' => $result->errors,
]);
}
}
Treat every write as partially successful and reconcile failures in a queue job with retry/backoff.
Upsert on an external id is the cleanest way to make integration idempotent:
$client->upsert('External_Id__c', $payloads);
Map SObjects to DTOs immediately. SOAP records are stdClass with Id, fieldsToNull, any and nested relationship objects. Convert to Eloquent models or value objects at the boundary; never let stdClass leak into views.
Queue the heavy work. Sync jobs belong on a queue with tries, backoff, and a failed() hook that stores the SOAP fault. Wrap in DB::transaction only for your local writes — Salesforce calls are not transactional with your database.
login.salesforce.com (or vice versa) produces confusing auth failures. Verify the login URL in config and that the WSDL matches the org.INVALID_VERSION or unexpected schema errors, regenerate the WSDL — no code change required, just a redeploy of the file.INVALID_SESSION_ID mid-request. Sessions expire. Catch it, re-login once, and retry the operation; don't retry blindly in a loop or you'll hammer the login endpoint.\') and validate Ids with a regex/SforceId check before interpolating.each()-era idioms). Test on 7.4 before upgrading, and vendor/fork the toolkit if you need to patch it.SoapFault, not exceptions you'll recognise. Wrap calls in try/catch (\SoapFault $e) and log $e->getMessage() plus $e->detail — the useful Salesforce error code lives in the detail, not the message.__getLastRequest/__getLastResponse dump) behind a config flag; the most common failures are TLS/cURL in the container, a relative WSDL path that breaks under config:cache, or a firewall blocking *.salesforce.com.php artisan config:cache. Env-backed config works fine, but any code that reads env() directly inside the package will return null once config is cached. Always read through config('salesforce.*').omniphx/forrest, or the official REST/Bulk APIs); keep this package only where the SOAP toolkit is a hard requirement or you're maintaining an existing Laravel 5 app. If you keep it, fork-and-pin and own the toolkit patches yourself.How can I help you explore Laravel packages today?