reprovinci/besimple-soap-client
A Laravel wrapper for the BeSimple SOAP client that simplifies configuring and using SOAP/WSDL web services in PHP applications. It provides service provider integration and convenient access for building and calling SOAP clients, useful for integrating legacy or third-party SOAP APIs.
Install and prerequisites
composer require reprovinci/besimple-soap-client
This package is a drop-in replacement/enhancement around PHP's native SoapClient. It is not a Laravel package: there is no service provider, no facade, no config file, and no auto-discovery. You wire it up yourself. Before anything else confirm the runtime has ext-soap enabled (php -m | grep soap) and that outbound SOAP/WSDL URLs are reachable — the library still depends on libxml/WSDL parsing under the hood.
First thing to check in the repo: the src/BeSimple/SoapClient/ tree. The entry points you will actually touch are:
SoapClientBuilder — fluent construction (WSDL, options, filters/plugins)SoapClientOptions — the equivalent array/options object if you use the non-builder pathWsSecurity/WsSecurity.php — the reason most people adopt this over vanilla SoapClientMtom/ and SwA/ — attachment supportFirst use case — a WSSE-secured WSDL call
use BeSimple\SoapClient\SoapClientBuilder;
use BeSimple\SoapClient\WsSecurityFilter;
$client = SoapClientBuilder::create()
->withWsdl('https://partner.example.com/service?wsdl')
->withOptions([
'trace' => true,
'exceptions' => true,
'cache_wsdl' => WSDL_CACHE_NONE, // during development
])
->withWsSecurityFilter(
WsSecurityFilter::create()
->withUsernameToken($username, $password)
->withPasswordDigest(true) // adjust per partner spec
)
->build();
$response = $client->SomeOperation(['id' => 42]);
If you only need vanilla SOAP, you do not need this package — use \SoapClient directly. Adopt it when you need WS-Security, MTOM/SwA attachments, or cookie/session-aware SOAP transports.
Wrap the client in a dedicated service class and bind it in the container. Do not inject the SOAP client into controllers; it is stateful (__getLastRequest, cookies) and slow to rebuild.
// app/Services/Partner/PartnerSoapClient.php
namespace App\Services\Partner;
use BeSimple\SoapClient\SoapClientBuilder;
use BeSimple\SoapClient\WsSecurityFilter;
class PartnerSoapClient
{
private \BeSimple\SoapClient\SoapClient $client;
public function __construct(array $config)
{
$this->client = SoapClientBuilder::create()
->withWsdl($config['wsdl'])
->withOptions([
'trace' => app()->isLocal(),
'exceptions' => true,
'connection_timeout' => $config['timeout'] ?? 10,
'cache_wsdl' => app()->isProduction()
? WSDL_CACHE_DISK
: WSDL_CACHE_NONE,
])
->withWsSecurityFilter(
WsSecurityFilter::create()
->withUsernameToken($config['user'], $config['pass'])
->withPasswordDigest($config['digest'])
)
->build();
}
public function __call(string $method, array $args)
{
return $this->client->{$method}(...$args);
}
}
// app/Providers/AppServiceProvider.php
$this->app->singleton(PartnerSoapClient::class, fn () =>
new PartnerSoapClient(config('services.partner'))
);
Put credentials and WSDL URLs in config/services.php, not in code. Treat it like any other external service credential.
Push the call onto a queue job for anything user-facing. SOAP partners are routinely slow (2–30s), and a stuck synchronous call blocks a PHP worker. Combine with tries, backoff, and a failed() handler that logs __getLastRequest() / __getLastResponse().
Attachments (MTOM / SwA) pattern. For file-upload operations, build the client with the MTOM or SwA filter, hand it a file path or SplFileObject, and confirm the partner actually negotiates application/xop+xml — many do not, in which case fall back to base64 in the SOAP body.
Caching layer. The package ships cache decorators for WSDL/metadata. Use disk cache in production (WSDL files change rarely). If you have many partners/WSDLs, key the cache by WSDL URL — most shared hosting setups will silently share a single cache directory otherwise.
Testing. Do not hit live partners in tests. Two practical approaches:
wsdl at a local static .wsdl fixture (committed under tests/fixtures/) and mock the HTTP transport, orPartnerSoapClient, bind a fake that returns canned response objects in tests.If you must assert the wire format, enable trace => true in a test-only config and inspect __getLastRequest().
php artisan vendor:publish, no config merging. Everything is manual. Budget time for that.BeSimple\SoapClient\*. If you also have besimple/soap-client or besimple/soap-common installed transitively, you can get class-not-found errors or silent class shadowing. Run composer why besimple/soap-common and pin/alias carefully.zendframework/zend-mime (or a laminas/laminas-mime fork). On modern PHP versions this is often the thing that blocks an upgrade. Check composer.json of the exact tag you install before committing.WsSecurityFilter password digest is fiddly. The server may expect PasswordText vs PasswordDigest, require a Timestamp with a specific expires, or reject a nonce format. Symptoms usually surface as an HTTP 500 with a SOAP fault like Security token could not be authenticated. Toggle withPasswordDigest() and inspect __getLastRequest() in a scratch script.Timestamp validation fails. Ensure NTP on the app server; consider widening expires.trace => true in production is expensive. It buffers full request/response payloads in memory. Keep it on only in local/staging, or gate it behind a config toggle.cache_wsdl => WSDL_CACHE_NONE in production is a footgun. Every call re-fetches the WSDL, adding latency and risking a hard failure if the partner's WSDL endpoint is briefly down. Use WSDL_CACHE_DISK, and document the cache path (default sys_get_temp_dir()) across multiple app servers.verify_peer to "fix" a failing call — install the partner's CA cert instead. Disabling it silently weakens every subsequent call from that client.SoapFault for SOAP-level errors, but transport errors (connection refused, timeout) may surface as generic exceptions from the HTTP layer. Wrap calls in try/catch covering both, log the full fault, and translate to a domain exception before it reaches your controller.__getLastRequest live on the client instance. If you bind it as a singleton (as above), be conscious that cookies persist across requests in the same worker. For long-running queue workers, consider rebuilding periodically or using a fresh instance per job.HttpPlug stack, not through middleware — there is no Laravel-style middleware concept here.composer.json, add an integration test against the real partner in staging, and keep a note of the upstream besimple/soap-client so you can swap forks if maintenance stalls.How can I help you explore Laravel packages today?