Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Salesforce Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Install

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.

Where to look first, in order

  1. The Bundle class (*Bundle.php). This tells you exactly which services are registered and under which IDs — that is your integration surface for Laravel.
  2. 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.
  3. The Configuration class (DependencyInjection/Configuration.php). This is the canonical list of config keys — do not trust README examples, they drift.
  4. The client wrapper itself. Identify the methods that proxy to Salesforce (query, describe, create, upsert, bulk job calls) — this is your day-to-day API.

First use case

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.


Implementation Patterns

1. Isolate the bundle behind your own interface

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.

2. Bootstrap the bundle's container once

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.

3. Queue every write

Salesforce API limits and latency make synchronous writes a production hazard in Laravel. Pattern:

  • Webhook / form handler → validate, persist locally, dispatch a job.
  • Job → call gateway upsert, use external ID field (External_Id__c) so retries are idempotent.
  • Failure → let the job fail, retry with backoff, and alert on 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()]);
    }
}

4. Separate read models from write models

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.

5. DTO / mapper layer

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.

6. Observability per call

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.


Gotchas and Tips

Framework mismatch is the biggest one

This bundle assumes Symfony's kernel, bundle registration, and DI conventions. In Laravel:

  • No auto-discovery — you must register and load extensions yourself.
  • Private services are inaccessible — check Resources/config; if the client is private, alias it in your Symfony container before compiling.
  • Parameter 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.

Maintenance risk (4 stars, 0 dependents)

  • Pin the version explicitly ("0.0.0"-style pre-1.0 is likely). Do not use ^ if the project looks unmaintained.
  • Vendor the source or fork under your VCS if Salesforce API versions or PHP versions start drifting.
  • Watch the Salesforce API version hardcoded in the bundle — Salesforce retires versions (~3 years) and the bundle will silently break when the endpoint is decommissioned.

Auth quirks

  • Sandbox vs production are different hosts: test.salesforce.com vs login.salesforce.com. Nearly every "auth fails" report comes from a config pointing at the wrong endpoint.
  • OAuth refresh — if the bundle's client doesn't persist refreshed tokens, you will burn through refresh tokens or get invalid_grant. Cache tokens externally (Cache::store('redis')->remember(...)).
  • IP restrictions and connected-app policies must be configured on the Salesforce side before anything works; the bundle cannot help you here.

API governor limits

  • SOQL queries: batch queries, use WHERE filters, never SELECT * on large objects.
  • If you get REQUEST_LIMIT_EXCEEDED, your queued jobs are hammering; add rate limiting (RateLimiter::for('salesforce', ...)).
  • Bulk API jobs are async — poll status; a synchronous expectation is a common source of "broken" reports.

Config quirks

  • Symfony config trees often reject unknown keys and require the exact nesting from Configuration.php. README examples frequently use different nesting.
  • Boolean config values in YAML/PHP need real booleans, not strings; "false" becomes truthy in loose comparisons inside the client.
  • Environment substitution: prefer reading credentials from env() in PHP config rather than YAML %env()% — easier to debug in Laravel.

Debugging playbook

  1. Resolve the raw client from the container (app('salesforce.container')->get(...)) and call it directly — bypass your gateway to isolate whether the fault is yours or the bundle's.
  2. Log the full Guzzle request (headers included, but redact Authorization).
  3. Test against a Developer Edition org before production; sandbox data shapes diverge.
  4. If compilation fails, dump $container->getServiceIds() — the service you want may not be registered at all under that name.

Extension points

  • Decorator pattern over the gateway is safer than overriding bundle services — you keep the bundle's updates.
  • Symfony compiler passes can modify the bundle's definitions at compile time if you need to inject a custom Guzzle client or HTTP middleware.
  • Custom 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.

When to walk away

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle