smartbox/besimple-soap
A Laravel package that brings SOAP web services into your app with a simple, expressive client. Build and send SOAP requests, parse responses, and integrate legacy APIs without boilerplate.
smartbox/besimple-soap is a maintained-in-name-only fork of the BeSimpleSoap toolchain: a set of libraries that wrap PHP's ext-soap to add WS-Security, MTOM/SwA attachments, WSDL tooling and a friendlier builder API. There is no Laravel service provider, no config file, no facade — you are wiring a low-level SOAP library. Treat it as vendored infrastructure, not as a package you composer require and forget.
Before you install anything, verify you actually need it:
php -m | grep -i soap # ext-soap is mandatory
php -m | grep -i openssl # required only for WS-Security signing/encryption
If your integration is a plain WSDL call with no WS-Security and no attachments, use ext-soap directly — it is maintained by php-src and needs zero extra dependency. Reach for this package when you need at least one of:
dateTime, duration, custom XSD typesInstall and pin exactly. v1.3.5 is the release to start from — it fixes a real WSDL-resolution caching bug (see Gotchas) and cleans up test-suite deprecations:
composer require smartbox/besimple-soap:^1.3.5
Do not float on dev-master. The package still ships no Laravel integration and the release cadence is glacial, so immediately fork it into your VCS ("repositories": [{"type": "vcs", ...}]) as soon as you need your first patch. Add it to your CI matrix on the exact PHP version you deploy — note that the 1.3.5 test matrix additions are Symfony 2.8 & 3.4, i.e. the project's CI is still anchored to a pre-PHP-8 ecosystem; do not read the changelog as evidence of modern PHP support.
First use case — a config-bound client. Create a service provider that builds and caches one client per endpoint:
// app/Providers/SoapServiceProvider.php
public function register(): void
{
$this->app->singleton(BillingSoapClient::class, fn () => SoapClientBuilder::create()
->withWsdl(config('soap.billing.wsdl'))
->withOptions([
'trace' => true, // needed for debugging
'exceptions' => true,
'cache_wsdl' => WSDL_CACHE_NONE, // see gotchas
'connection_timeout' => 10,
'classmap' => require app_path('Soap/billing-classmap.php'),
])
->build());
}
Look first at the generated classmap and the WSDL's targetNamespace — almost every integration bug later traces back to one of those two. If the WSDL pulls in <xsd:import>s, look at where the resolved copies land before you go further; that path is exactly what 1.3.5 changed.
Wrap every remote in a contract. Do not inject SoapClient into controllers, jobs or models. Define a plain interface and an adapter:
interface BillingSoapClient
{
public function getInvoice(string $id): InvoiceResponse;
}
This is the single highest-leverage decision with this package, because it makes the SOAP layer swappable and lets you fake it in tests (you cannot Http::fake() a raw socket-based SoapClient).
Config-driven endpoint registry. Put WSDL URLs, credentials and per-environment overrides in config/soap.php, never in code. Enterprise partners hand you different WSDLs per environment, and WS-Security passwords rotate.
DTO hydration at the boundary. ext-soap (and therefore this package) returns stdClass graphs, not your models. Hydrate immediately in the adapter:
$raw = $this->client->getInvoice(['invoiceId' => $id]);
return InvoiceResponse::fromSoap($raw); // one place knows the SOAP shape
On the way out, build arrays deliberately. Repeated elements need a list with sequential integer keys; a single-element result must still be [0 => ...] or it serializes as an object instead of an array.
WS-Security for enterprise partners. Attach it at build time:
$builder = SoapClientBuilder::create()->withWsdl($wsdl)->withWsSecurity();
// username token
$builder->withWsSecurityUsernameToken($user, $pass, /* digest */ true);
// or X.509 signing
$builder->withWsSecurityCertificate($certPath, $privateKeyPath, $passphrase);
Keep the credentials in config/soap.php, and make sure the server's clock is NTP-synced — UsernameToken timestamps default to a short TTL and drift produces opaque faults.
Attachments via MTOM/SwA. For document uploads, stream from Laravel's Storage into a SoapAttachment rather than file_get_contents — payloads in these integrations are routinely tens of megabytes and base64 inflates them by a third.
Queue the calls, but understand idempotency. SOAP operations are usually not idempotent. If you dispatch SendOrderJob, set tries = 1 unless the partner API explicitly supports a dedupe key; a retried createInvoice silently double-bills.
Pre-resolve imported WSDLs at build time. Even with the 1.3.5 fix, the safest pattern for import-heavy WSDLs remains: download the whole tree into storage/app/wsdl during deploy, rewrite the <xsd:import schemaLocation="..."> entries to relative local paths, and point withWsdl() at the local entry file. That makes the build hermetic, removes intermittent network failures from your test suite, and makes the WSDL-cache behaviour irrelevant.
Server side, if you are the producer. You can expose a Laravel route that delegates to the server builder:
Route::post('/soap/billing', function () {
$server = SoapServerBuilder::create()
->withWsdl(storage_path('wsdl/billing.wsdl'))
->withService(new BillingService())
->build();
$server->handle();
})->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
Return the raw response — do not wrap it in a JsonResponse or a view.
Observability decorator. Because calls bypass Guzzle, Laravel Telescope and the HTTP client events see nothing. Add a decorator around your interface that logs elapsed time, operation name and, on \SoapFault, __getLastRequest() / __getLastResponse() (enabled by trace => true). Redact credentials before logging.
Testing. Bind a fake implementation of your interface in TestCase::setUp(). For genuine integration tests, run the generated SoapServer locally against a WSDL fixture and point the client at it — this is the only reliable way to catch namespace and soap_version mismatches before they hit staging.
\SoapClient internals — so if you saw deprecation noise on PHP 8.1+, verify it is actually gone against your PHP version rather than assuming. Budget a fork-and-patch afternoon; evaluate phpro/soap-client or native ext-soap before adopting for greenfield work.cache_wsdl mode other than WSDL_CACHE_NONE with a WSDL that <xsd:import>s other files, older versions wrote incomplete/incorrect resolved documents into the cache directory. Symptom: the client re-fetches the imports on every request, or fails on a cache entry that looks present but is unusable. When upgrading, clear the WSDL cache directory (sys_get_temp_dir() or your configured cache_dir) — stale artifacts written by the old code will otherwise survive the upgrade and keep failing.cache_wsdl still bites in containers. The default WSDL_CACHE_BOTH writes to sys_get_temp_dir(). Read-only or ephemeral filesystems produce "failed to load external entity" or serve a stale WSDL forever after the partner updates theirs. Set WSDL_CACHE_NONE or an explicit cache_dir and clear it on deploy. 1.3.5 makes the saving correct; it does not make an unwritable temp dir writable.login/password in withOptions() authenticate the WSDL download; withBasicAuth() / WS-Security authenticates the service call. Mixing these up gives a 401 on one and a 500 on the other.timeout option. SoapClient honours connection_timeout and PHP's default_socket_timeout, not a timeout key. A missing setting means your queue worker hangs for the process default.soap_version mismatch (SOAP_1_1 vs SOAP_1_2) or a response wrapped in a proxy/error page. Log the raw response before assuming the WSDL is wrong.exceptions => true is not optional. Without it you get silent false returns. With it you get \SoapFault, which Laravel's handler renders as a 500 unless you catch it in the adapter and translate to a domain exception.__getLastRequest() returns null if trace => true was not set at construction — you cannot enable it later.['item' => [0 => $a]] explicitly; associative keys produce a struct rather than a repeated element. This is the most common cause of partner-side schema validation failures.xsi:nil and attributes. Nulls are omitted by default. To send an explicit nil or an XML attribute you need SoapVar with XSD_ANYXML or the leading-underscore attribute array form the package's type converters understand.Security fault.TypeConverterInterface (register via withTypeConverters()) is the clean hook for custom XSD types and is far safer than post-processing stdClass. Subclassing the client's __doRequest() is the pragmatic hook for request signing not covered by the builder, and for capturing envelopes in tests.<xsd:import> across relative URLs) require the parser to be able to resolve those URLs. Pre-download the whole WSDL tree into storage/app/wsdl and point at the local file; it makes builds hermetic and removes a class of intermittent network failures from your test suite — and it is now the mitigation of choice rather than relying on the cache layer, even post-1.3.5.How can I help you explore Laravel packages today?