localfr/salesforce-client-bundle
Symfony bundle that wires a Salesforce API client into your PHP application, providing ready-made service definitions and configuration so you can authenticate and query Salesforce from your own code with minimal setup.
Do the reality check first. localfr/salesforce-client-bundle is a Symfony bundle: it extends Symfony\Component\HttpKernel\Bundle\Bundle and ships DependencyInjection/Configuration.php plus a Resources/config/services.yaml. Laravel has no bundle loader, so composer require inside a Laravel app gives you classes you must wire by hand — not a working integration, and no SalesforceClient facade will appear. Treat this repo as a reference implementation of a Salesforce auth/API client and a source of code you can lift, not as a drop-in dependency.
Minimal steps that actually work:
composer require localfr/salesforce-client-bundle and run composer show localfr/salesforce-client-bundle. The require block tells you which underlying client package is doing the real work (likely localfr/salesforce-client or phpforce-style HTTP wrapper). That inner package is what you actually bind in Laravel; the bundle is just DI glue.DependencyInjection/Configuration.php before anything else. It is the authoritative list of credential/config keys — consumer key, consumer secret, username, password, security token, login/instance URL, API version. Every key you'll put in config/services.php is defined there.HttpClientInterface it uses, and whether a cache/token-storage adapter is wired in.First use case — authenticate and query. The classic flow is the OAuth2 username-password grant with the security token appended to the password. Wire it once in a service provider:
// config/services.php
'salesforce' => [
'login_url' => env('SF_LOGIN_URL', 'https://login.salesforce.com'),
'client_id' => env('SF_CLIENT_ID'),
'client_secret' => env('SF_CLIENT_SECRET'),
'username' => env('SF_USERNAME'),
'password' => env('SF_PASSWORD'),
'security_token' => env('SF_SECURITY_TOKEN'),
'version' => env('SF_API_VERSION', 'v59.0'),
],
// AppServiceProvider::register()
$this->app->singleton(SalesforceClient::class, fn ($app) =>
new SalesforceClient($app['config']->get('salesforce'))
);
Then hit GET /services/data/{version}/query?q=SELECT+Id,+Name+FROM+Account+LIMIT+10 with Authorization: Bearer {access_token}.
Pattern A — extract the client, drop the bundle. Vendor the HTTP/auth class from the package into app/Services/Salesforce/ (MIT license makes this fine) and bind it in a service provider. You keep the battle-tested request/response handling and lose the Symfony container coupling. This is the path most Laravel shops take.
Pattern B — Symfony sidecar. Stand up a 200-line Symfony app with this bundle installed, expose two or three JSON endpoints (/query, /upsert), and call it from Laravel with Http::withToken(config('sidecar.token')). Worth it only if you need the bundle's Bulk API or streaming support and don't want to reimplement it.
Pattern C — token caching with a retry layer. Salesforce access tokens expire (typically ~2h) and the API returns 401 with INVALID_SESSION_ID. Cache the token in Laravel's cache under the instance URL key, and use Http::retry(2, 100)->beforeSending() or a small PendingRequest macro that catches 401, refreshes, and replays once. Do not re-authenticate per request; login calls count against limits.
Pattern D — queued sync jobs with external-ID upserts. Dispatch SyncContactToSalesforce jobs on ->onQueue('salesforce'). Use PATCH /sobjects/Contact/External_Id__c/{value} so retries are idempotent. Map Eloquent models to SObjects with a toSalesforce(): array method and $casts for dates/booleans — Salesforce rejects '1' where it expects true.
Pattern E — choose the right API. Under ~200 records use the Composite API (/composite/sobjects) to batch in one round trip. Above that, use Bulk API 2.0 (/jobs/ingest) and poll state until JobComplete. Do not loop 5,000 single-record REST calls; you'll burn the daily API allocation and time out workers.
Pattern F — test with Http::fake(). Because the client ultimately speaks HTTP, you can stub /services/data/* responses and assert your mapping layer without touching a sandbox org. Keep one integration test against a Salesforce Developer Edition behind an env flag.
Pattern G — inbound webhooks/Outbound Messages. Salesforce Outbound Messages are SOAP; newer orgs use Platform Events or Flow HTTP callouts. Receive them in a controller, verify the shared secret, and dispatch() immediately — never process inline.
password + security_token. Forgetting this produces INVALID_LOGIN and hours of confusion. If your org IP-relaxes the token, it's blank — but don't assume.q=. Bind via the query endpoint's parameters or escape ', \, and control chars. The Symfony bundle doesn't protect you here and neither will a naive port.Id, Name, CreatedDate, MyField__c. id returns INVALID_FIELD. Normalize keys on the way in and out; don't rely on Str::camel for the outbound payload.SF_API_VERSION explicitly and upgrade deliberately — Salesforce retires versions on a published schedule./services/data/vXX.X/limits. Instrument your client with a counter and log Sforce-Limit-Info response headers. Batch aggressively.POST /jobs/ingest returns a job ID; you must poll GET /jobs/ingest/{id} until JobComplete, then fetch successfulResults/failedResults CSVs. Don't assume the 200 means records landed.symfony/* constraints against your PHP version before committing — the Jan 2025 release targets Symfony 6/7, and this package has zero dependents, so nobody else has hit the edge cases for you.$this->app->extend() and constructor injection — don't try to mimic the bundle's tag-based wiring.debug level behind a SF_DEBUG flag, redacting Authorization. The failure modes are almost always auth (token/credentials), field names, or validation rules — the raw 400 body tells you which, and Laravel's Http::dd() is your fastest tool.How can I help you explore Laravel packages today?