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

genesis-global/salesforce-bundle

A Laravel package that streamlines Salesforce integration with configuration, service bindings, and helpers for authenticating, querying, and syncing records through the Salesforce REST API, reducing boilerplate for PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: genesis-global/salesforce-bundle is a Symfony bundle, not a Laravel package. It has no ServiceProvider, no config/*.php, and no package auto-discovery. In practice you will spend most of your time in the underlying REST client it wraps (the genesis-global/salesforce-rest-client library pulled in as a dependency) and treat the bundle as configuration boilerplate you replicate by hand.

Minimal steps

  1. Install the bundle (or, preferably, just the underlying client library — see below):
    composer require genesis-global/salesforce-bundle
    
  2. Open vendor/genesis-global/salesforce-bundle/ and read, in this order:
    • README.md (auth options and the service name it exposes)
    • DependencyInjection/Configuration.php — the authoritative list of config keys
    • Resources/config/services.yml — the actual class graph, i.e. which constructor arguments you must supply manually
  3. Do not register the Bundle class in Laravel. Instead, replicate its service definitions in an AppServiceProvider. The bundle's own Bundle class extends Symfony\Component\HttpKernel\Bundle\Bundle, which drags in symfony/http-kernel you don't otherwise need.

Preferred wiring for Laravel

// config/services.php
'salesforce' => [
    'login_url'      => env('SALESFORCE_LOGIN_URL', 'https://login.salesforce.com'),
    'client_id'      => env('SALESFORCE_CLIENT_ID'),
    'client_secret'  => env('SALESFORCE_CLIENT_SECRET'),
    'username'       => env('SALESFORCE_USERNAME'),
    'password'       => env('SALESFORCE_PASSWORD'),
    'security_token' => env('SALESFORCE_SECURITY_TOKEN'),
    'api_version'    => env('SALESFORCE_API_VERSION', 'v52.0'),
],
// app/Providers/SalesforceServiceProvider.php
public function register(): void
{
    $this->app->singleton(SalesforceClient::class, function ($app) {
        $cfg = $app['config']['services.salesforce'];

        return new SalesforceClient(/* args mirroring Resources/config/services.yml */);
    });

    $this->app->alias(SalesforceClient::class, 'salesforce');
}

Bind as a singleton — that is the single most important decision. Each instantiation triggers a login round-trip against Salesforce, and repeated authentication is what blows through your API call budget.

First use case: a queued job that runs a SOQL query and upserts results into a local table. Prove auth + a simple SELECT Id, Name FROM Account LIMIT 10 end-to-end before building anything layered on top.

Implementation Patterns

Put a gateway between your app and the client. The raw client returns arrays/stdClass and throws generic exceptions. Wrap it once so the rest of the codebase never sees the vendor types:

namespace App\Services\Salesforce;

class SalesforceGateway
{
    public function __construct(private SalesforceClient $client) {}

    /** @return array<int, array<string, mixed>> */
    public function query(string $soql): array
    {
        $rows = [];
        $response = $this->client->query($soql);

        while (true) {
            array_push($rows, ...$response['records']);

            if (empty($response['nextRecordsUrl'])) {
                break;
            }
            $response = $this->client->queryMore($response['nextRecordsUrl']);
        }

        return $rows;
    }

    public function upsert(string $sobject, string $externalIdField, array $payload): array
    {
        return $this->client->upsert($sobject, $externalIdField, $payload);
    }
}

Paginating with nextRecordsUrl is mandatory — SOQL silently returns only the first batch (typically 2,000 records) and the client will not loop for you.

Sync out, not in. The common Laravel workflow is Salesforce → local database for read-heavy reporting, and local database → Salesforce for writes. Keep both directions idempotent by always writing to Salesforce via upsert on your own external-ID field rather than create, so a retried job doesn't duplicate records.

One job per object, chunked. Use Bus::batch() with one job per SOQL page or per 200-record write chunk. Salesforce enforces per-transaction and 24-hour API request limits; small chunks make retries cheap and keep failed_jobs readable.

Map to DTOs, not Eloquent directly. Salesforce field names are PascalCase and __c-suffixed, and relationship fields come back as nested objects. A SalesforceAccountData::fromArray() constructor keeps the mapping in one reviewable place and shields your models from API version churn.

Test by faking the gateway, not the HTTP layer. Bind an in-memory implementation of an interface in tests:

$this->app->bind(SalesforceGatewayInterface::class, FakeSalesforceGateway::class);

Salesforce sandboxes are slow and rate-limited; do not point your test suite at one.

Separate credentials per environment. Sandbox uses https://test.salesforce.com, production uses https://login.salesforce.com. Getting this wrong in a .env is the most common cause of "works locally, writes to prod in staging."

Gotchas and Tips

  • It's abandoned-adjacent. Last release was November 2020, 11 stars, zero dependents. Treat it as unmaintained source you may need to fork. Check composer.json for its PHP and Guzzle constraints before installing — Guzzle 6/7 conflicts with modern Laravel are likely, and PHP 8 type declarations on Symfony's BundleInterface will break the bundle class if you try to register it under Symfony 6 components.
  • The config keys you see in the README may not match Configuration.php. That file is the source of truth for auth mode (password + security token vs. OAuth client_credentials/refresh_token). Read it before guessing env var names.
  • Escape SOQL strings yourself. The client passes SOQL through verbatim; there is no query builder and no binding. Escape single quotes and backslashes at the gateway boundary, or a value containing ' becomes an injection vector and a syntax error.
  • INVALID_SESSION_ID means your cached token expired (typically after ~2 hours). Detect it and re-authenticate once rather than surfacing a 500. If you see this error frequently, your singleton binding is probably being re-created per request rather than per process — check for scoped vs singleton, and for Octane workers holding stale tokens.
  • Salesforce returns HTTP 200 with failures inside the body. Check the success flag and the errors array on each record of a write response; a batch can partially succeed, and the client will not throw.
  • 15- vs 18-character IDs. REST returns 18-character IDs in some payloads and 15 in others (and in CSV exports). Use substr($id, 0, 15) for comparisons so you don't create duplicate local rows.
  • Debugging: inject a Guzzle middleware that logs request/response bodies to a dedicated salesforce log channel with a low retention window — payloads contain customer data. Log the Sforce-Limit-Info response header; it tells you exactly how many API calls remain per day.
  • Extension point: the cleanest place to add retries, logging, and backoff is a decorator around your SalesforceGateway, not a modification of the vendor client. That way the bundle can be replaced with a hand-rolled client (or a maintained alternative like omniphx/forrest) later without touching call sites.
  • Consider not using this package at all. For a Laravel codebase, omniphx/forrest or a ~200-line hand-rolled client over Guzzle against the Salesforce REST API will be less work than adapting a dormant Symfony bundle — and you'll own the token-caching and pagination behavior instead of inferring it from YAML.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
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