oowlish/salesforce-bundle
Oowlish Salesforce Bundle provides Symfony integration for Salesforce, with configurable clients and services for authentication, querying records, and working with Salesforce objects. Use it to connect PHP applications to Salesforce data and workflows.
First, the reality check: despite the description, oowlish/salesforce-bundle is a Symfony Bundle, not a Laravel package. There is no service provider, no config/salesforce.php auto-merge, and no Facade. In a Laravel app you will consume it the way you consume any Symfony bundle: register the bundle in a kernel (or a thin standalone container) and pull the underlying client out as a plain service. Budget a short adaptation step before you get value.
composer require oowlish/salesforce-bundle
You will also need whichever Salesforce client the bundle wraps (typically oowlish/salesforce-php-client or an injected Guzzle-based client). Check composer.json of the bundle — the transitive HTTP client is the thing that actually talks to Salesforce; the bundle is mostly wiring.
*Bundle.php). This tells you exactly which services are registered and under which IDs — that is your integration surface for Laravel.Resources/config/services.* (XML/YAML/PHP). Service IDs, aliases, and constructor arguments live here. If a class is private, you must alias or decorate it before you can resolve it.DependencyInjection/Configuration.php). This is the canonical list of config keys — do not trust README examples, they drift.query, describe, create, upsert, bulk job calls) — this is your day-to-day API.The fastest win is a read-only pull: authenticate against a sandbox org and run a SELECT on a small object, then materialize the result as Eloquent models via a mapper. Do not start with write/sync — get auth and query working end-to-end first, because 90% of setup pain is credentials, OAuth, and endpoint selection.
// Rough shape — actual service ID comes from services.xml
$client = app()->make(\Oowlish\Salesforce\Client::class);
$records = $client->query('SELECT Id, Name, Email FROM Lead LIMIT 10');
If your Laravel container cannot resolve the class (private service, missing constructor args), that is normal — see Gotchas.
Never inject the bundle's classes directly into controllers or jobs. The bundle has 4 stars and 0 dependents; treat it as a replaceable dependency.
interface SalesforceGateway
{
public function query(string $soql, array $bindings = []): array;
public function upsert(string $object, string $externalIdField, array $records): array;
}
final class BundleSalesforceGateway implements SalesforceGateway
{
public function __construct(private \Oowlish\Salesforce\Client $client) {}
// ...
}
Bind in AppServiceProvider:
$this->app->singleton(SalesforceGateway::class, BundleSalesforceGateway::class);
This is the single most important pattern. It lets you swap to dcarbone/php-force-client or direct REST calls without touching 200 call sites when the bundle goes stale.
The clean way in Laravel is a small dedicated bootstrap that builds a Symfony ContainerBuilder, registers the bundle, and exposes a single PSR-11 container to Laravel:
// AppServiceProvider::register()
$this->app->singleton('salesforce.container', function ($app) {
$container = new \Symfony\Component\DependencyInjection\ContainerBuilder();
$container->setParameter('kernel.environment', app()->environment());
$container->registerExtension(new \Oowlish\SalesforceBundle\DependencyInjection\SalesforceExtension());
$container->loadFromExtension('salesforce', config('salesforce'));
$container->compile();
return $container;
});
$this->app->singleton(\Oowlish\Salesforce\Client::class, fn ($app) => $app['salesforce.container']->get('salesforce.client'));
Cache the compiled container (or serialize a resolved client) — compiling on every request is slow.
Salesforce API limits and latency make synchronous writes a production hazard in Laravel. Pattern:
upsert, use external ID field (External_Id__c) so retries are idempotent.failed_jobs.final class SyncLeadToSalesforce implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public int $tries = 5;
public array $backoff = [10, 60, 300, 900];
public function handle(SalesforceGateway $sf): void
{
$sf->upsert('Lead', 'External_Id__c', [$this->lead->toSalesforcePayload()]);
}
}
Use the bundle for two distinct flows:
| Flow | API | Pattern |
|---|---|---|
| Reporting / lookup | REST query (SOQL) |
Cache with Cache::remember, short TTL (60–300s) |
| Bulk sync | Bulk API (if wrapped) | Chunked jobs, 200–10k records per batch |
| Single-record write | REST sobjects |
Queued, idempotent via external ID |
| Schema discovery | describeSObject |
One-time, cache for hours |
Do not do reporting reads on the request path — paginate locally, sync into your DB, query your DB.
Salesforce payloads are stringly-typed, inconsistent about nulls, and use attributes metadata. Map to typed DTOs immediately:
final class LeadDto
{
public static function fromSalesforce(array $row): self { /* ... */ }
public function toSalesforce(): array { /* ... */ }
}
This is where date/timezone, number precision, and picklist validation bugs get caught.
Wrap the gateway with a decorator that logs method, SOQL, duration, and HTTP status. In Laravel, a Log::withContext in the decorator is enough; for deeper inspection, tap the underlying Guzzle client's HandlerStack middleware to emit Request/Response events — critical for debugging the bundle's auth.
This bundle assumes Symfony's kernel, bundle registration, and DI conventions. In Laravel:
Resources/config; if the client is private, alias it in your Symfony container before compiling.kernel.debug / kernel.environment are usually required — supply them or compilation throws.If that bridge feels heavy, honestly evaluate skipping the bundle and using the underlying client directly. The bundle is thin; you may be paying a Symfony tax for little gain.
"0.0.0"-style pre-1.0 is likely). Do not use ^ if the project looks unmaintained.test.salesforce.com vs login.salesforce.com. Nearly every "auth fails" report comes from a config pointing at the wrong endpoint.invalid_grant. Cache tokens externally (Cache::store('redis')->remember(...)).WHERE filters, never SELECT * on large objects.REQUEST_LIMIT_EXCEEDED, your queued jobs are hammering; add rate limiting (RateLimiter::for('salesforce', ...)).Configuration.php. README examples frequently use different nesting."false" becomes truthy in loose comparisons inside the client.env() in PHP config rather than YAML %env()% — easier to debug in Laravel.app('salesforce.container')->get(...)) and call it directly — bypass your gateway to isolate whether the fault is yours or the bundle's.Authorization).$container->getServiceIds() — the service you want may not be registered at all under that name.Client binding — in Laravel, rebind the class name to your subclass; the bundle will pick it up if the service references by ID resolves through your binding.If the bundle's underlying client is the only substantive code and it pins an old Guzzle/PHP version, drop it. A 150-line REST/gateway class with Http::withToken() in Laravel will outlive this dependency and be easier to test. Use the bundle only if it already handles OAuth refresh and Bulk API polling in a way you do not want to rebuild — verify both before committing.
How can I help you explore Laravel packages today?