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

Soap Client Laravel Package

xrow/soap-client

A SOAP client package for Laravel and PHP that simplifies building and sending SOAP requests, working with WSDL definitions, and handling responses. Integrates with Laravel's container and configuration for cleaner, more testable web service integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and satisfy the hard requirement. The package is a thin wrapper over PHP's SoapClient, so ext-soap must be enabled — this is the single most common first-run failure.

composer require xrow/soap-client
php -m | grep soap   # must print "soap"

Get a WSDL before you write any code. Salesforce does not expose a public WSDL — it is generated per-org. In Salesforce: Setup → API → Generate Enterprise WSDL (typed against your org's objects and custom fields) or grab the Partner WSDL (untyped, generic sObject, works across orgs). Drop the file in storage/app/salesforce/ and keep it out of git.

Where to look first. Read the client class source before binding it — the library is small, unmaintained since 2015, and has essentially zero downstream users, so there is no community documentation to fall back on. You will learn more from the 150 lines of source than from any README. Confirm the constructor signature (WSDL path, options array) and whether it exposes a login() helper or expects you to call SoapClient::__soapCall directly.

First use case — authenticate and read. Wire credentials through config, bind the client as a singleton, and prove connectivity with a single query():

// config/salesforce.php
return [
    'wsdl'     => storage_path('app/salesforce/enterprise.wsdl'),
    'username' => env('SF_USERNAME'),
    'password' => env('SF_PASSWORD'),
    'token'    => env('SF_SECURITY_TOKEN'),
    'endpoint' => env('SF_ENDPOINT', 'https://login.salesforce.com'),
    'cache'    => storage_path('framework/cache/wsdl'),
];
$this->app->singleton(SalesforceClient::class, fn () => new SalesforceClient(config('salesforce')));

// first smoke test
$result = app(SalesforceClient::class)->query('SELECT Id, Name FROM Account LIMIT 5');

If that returns records, everything else is a variation on it.

Implementation Patterns

Never inject the client into a controller. The client is stateful (it holds a session ID and a serverUrl returned by login()), so treat it as infrastructure. Put a SalesforceService or per-object repository in front of it and inject that. This also gives you one seam to fake in tests.

Cache the session, not just the client. login() consumes an API call and returns a session ID plus an instance URL that expires (two hours by default). Store the pair in the cache with a TTL comfortably under the timeout, and re-authenticate on INVALID_SESSION_ID:

$session = Cache::remember('sf:session', now()->addMinutes(90), fn () => $this->client->login());

Page query() explicitly. A single query() returns at most 2,000 records and a queryLocator. queryMore($locator) walks the rest. Wrap this in a generator so callers never see the locator:

foreach ($this->salesforce->each('SELECT Id, Email FROM Contact WHERE ...') as $record) { ... }

Batch writes at 200 records. Salesforce caps a single SOAP create/update/upsert call at 200 objects. Chunk with collect($rows)->chunk(200) and send one call per chunk, collecting per-record errors rather than aborting the batch — Salesforce happily accepts 199 and rejects 1.

Push outbound work onto the queue. API calls are slow (200–800 ms typical) and rate-limited by a rolling 24-hour org allocation. Dispatch a job per sync; add retry() with exponential backoff and use RateLimited middleware if the org is shared with other integrations.

Inbound: use Outbound Messages as the trigger. Salesforce can POST a SOAP message to a Laravel route on record change. Accept it, return the SOAP acknowledgement envelope immediately, and hand the payload to a job — Salesforce retries non-2xx responses for 24 hours, so a slow handler will get you duplicate deliveries.

Pick the right WSDL per job. Enterprise is typed and pleasant to work with but breaks whenever an admin adds a custom field — the WSDL must be regenerated and redeployed. Partner is stable but everything arrives as generic sObject/stdClass. Most teams use Enterprise for a fixed set of objects and Partner for the one integration that must survive schema churn.

Cache describeSObject() results. Field metadata changes rarely, takes seconds to fetch, and is usually only needed for dynamic mapping. A daily cache entry is fine.

Gotchas and Tips

  • It is abandoned. Last release May 2015, one star, zero dependents. There will be no fix for a PHP 8 break. Budget a half-day to audit the source: look for curly-brace string offsets ($str{0}, fatal in PHP 8), missing declare(strict_types=1), and any hardcoded API version string. If it is a few hundred lines, consider vendoring it or copying the useful bits into app/Services and dropping the dependency — you lose nothing but an update path that does not exist anyway.
  • The pinned API version will expire. Salesforce retires API versions roughly three years after release. If the client hardcodes something like 29.0, calls begin failing with INVALID_API_VERSION well before the code looks old. Make the version configurable or patch the WSDL's version attribute at boot.
  • WSDL parsing is slow and memory-hungry. An Enterprise WSDL for a large org can be tens of MB. Enable wsdl_cache and point cache_wsdl / wsdl_cache_dir at a writable path — the default sys_get_temp_dir() is often unwritable in containers and fails with a confusing SOAP-ERROR: Parsing WSDL.
  • Regenerate the WSDL when schema changes. A new custom field added in production is invisible until you re-download the WSDL. This is the #1 "why is the field null" bug. Add WSDL regeneration to your release checklist, or switch that integration to the Partner WSDL.
  • SOAP Fault is not Exception. Catch \SoapFault and map errorCode strings (INVALID_FIELD, ENTITY_IS_DELETED, UNABLE_TO_LOCK_ROW, REQUEST_LIMIT_EXCEEDED) into your own exception hierarchy. UNABLE_TO_LOCK_ROW is transient — retry it; INVALID_FIELD is not.
  • Serialization quirks. Booleans arrive as strings "true"/"false" and dates as ISO-8601 with a timezone offset; cast explicitly on the way in and always send DateTime in UTC. null fields are omitted from responses rather than returned as null, so use data_get($record, 'Field') rather than property access.
  • Field-level security applies to the integration user. A field that exists in the WSDL can still come back missing if the user's profile cannot read it. Symptom: a mysterious INVALID_FIELD on write or a silently absent key on read. Check the profile before debugging your code.
  • SOAP only — no Bulk API. Bulk inserts of 50k+ records over SOAP are painful and will burn your daily API allocation. If the package's use case drifts toward bulk sync, reach for Bulk API 2.0 over REST instead and keep this client for real-time single-record work.
  • Test at the service boundary, not the SOAP layer. Because SoapClient is final-ish and awkward to mock, define a SalesforceGateway interface, bind the package behind it, and swap a fake in tests. Do not attempt to stub __soapCall.
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.
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
uni-method/symfony-lib