laravel-soap maintained by siberfx
Laravel SOAP
A modern, fluent SOAP client for Laravel. Register SOAP services once — in config or in code —
and call them anywhere with Soap::call('Service.Method', $arguments). Includes class maps,
SOAP headers, authentication, TLS/proxy options, events, readable exceptions and a
testing fake so your test suite never touches a real SOAP endpoint.
Inspired by notfalsedev/laravel-soap, rewritten for PHP 8.4+ and Laravel 12/13.
Contents
- Requirements
- Installation
- Quick start
- Registering services
- Service options reference
- Calling operations
- Error handling
- Events
- Testing
- Customisation
- Migrating from notfalsedev/laravel-soap
- Changelog
- License
Requirements
| Package | PHP | Laravel |
|---|---|---|
| 1.x | 8.4, 8.5, 8.6 | 12.x, 13.x |
The PHP soap extension must be enabled.
Installation
composer require siberfx/laravel-soap
The service provider and the Soap facade are registered automatically via package discovery.
Publish the configuration file (optional):
php artisan vendor:publish --tag=soap-config
Quick start
use Siberfx\Soap\Facades\Soap;
use Siberfx\Soap\Service;
Soap::add('Currency', fn (Service $service) => $service
->wsdl('https://currencyconverter.kowabunga.net/converter.asmx?WSDL')
->trace()
);
$amount = Soap::call('Currency.GetConversionAmount', [[
'CurrencyFrom' => 'USD',
'CurrencyTo' => 'EUR',
'RateDate' => '2026-09-30',
'Amount' => '1000',
]]);
Registering services
A service is a named SOAP endpoint. Each service gets its own lazily created, cached
\SoapClient, built the first time you call it.
In code
Register services in a service provider's boot() method (for example AppServiceProvider):
use Siberfx\Soap\Facades\Soap;
use Siberfx\Soap\Service;
public function boot(): void
{
Soap::add('Weather', function (Service $service) {
$service
->wsdl(config('services.weather.wsdl'))
->soapVersion(SOAP_1_2)
->basicAuth(config('services.weather.user'), config('services.weather.password'))
->connectionTimeout(10)
->cache(WSDL_CACHE_BOTH);
});
}
add() also accepts the same array format used in the config file:
Soap::add('Weather', [
'wsdl' => 'https://example.com/weather?wsdl',
'trace' => true,
]);
Registering the same name twice throws ServiceAlreadyExistsException. Use Soap::forget('Weather')
first if you really need to replace a service. Other helpers:
Soap::has('Weather'); // bool
Soap::service('Weather'); // Siberfx\Soap\Service
Soap::services(); // array<string, Service>
Soap::addMany([
'Weather' => [...],
'Currency' => fn (Service $s) => $s->wsdl('...'),
]);
You can also inject Siberfx\Soap\SoapWrapper instead of using the facade — it is bound as a
singleton (also aliased as soap).
In config
Services in config/soap.php are registered automatically:
'services' => [
'currency' => [
'wsdl' => env('CURRENCY_WSDL'),
'trace' => true,
'classmap' => [
'GetConversionAmount' => App\Soap\Types\GetConversionAmount::class,
],
'login' => env('CURRENCY_USER'),
'password' => env('CURRENCY_PASSWORD'),
],
],
Supported keys: wsdl, location, uri, soap_version, trace, cache (or cache_wsdl),
classmap, typemap, headers, stream_context, verify_ssl, client and options.
Any other key is passed as-is to the native \SoapClient (e.g. login, password,
authentication, proxy_host, local_cert, user_agent, keep_alive). null values are ignored,
so unset environment variables fall back to the defaults.
Global defaults
soap.defaults is applied to every service — from config or code — before the service's own settings:
'defaults' => [
'trace' => (bool) env('SOAP_TRACE', false),
'soap_version' => SOAP_1_1,
'cache' => (int) env('SOAP_WSDL_CACHE', WSDL_CACHE_BOTH),
'connection_timeout' => (int) env('SOAP_CONNECTION_TIMEOUT', 30),
'verify_ssl' => (bool) env('SOAP_VERIFY_SSL', true),
],
| Env variable | Default | Description |
|---|---|---|
SOAP_TRACE |
false |
Keep raw request/response XML for every service. |
SOAP_WSDL_CACHE |
3 |
0 none, 1 disk, 2 memory, 3 both. |
SOAP_CONNECTION_TIMEOUT |
30 |
Seconds to wait while connecting. |
SOAP_VERIFY_SSL |
true |
Set to false only for local/self-signed hosts. |
SOAP_EVENTS |
true |
Dispatch request/response events. |
Tip: during development
SOAP_WSDL_CACHE=0avoids stale WSDLs after a remote change.
Non-WSDL mode
For services without a WSDL, provide location and uri instead:
Soap::add('Legacy', fn (Service $s) => $s
->location('https://legacy.example.com/soap')
->uri('urn:legacy-service')
);
A service with neither a WSDL nor both location and uri throws
InvalidServiceConfigurationException when its client is built.
Service options reference
All methods return the service, so they can be chained.
| Method | \SoapClient option / effect |
|---|---|
wsdl(?string $wsdl) |
WSDL URL or local path (null = non-WSDL mode) |
location(string $url) |
location — endpoint, overrides the WSDL endpoint |
uri(string $namespace) |
uri — target namespace (non-WSDL mode) |
soapVersion(int $version) |
soap_version — SOAP_1_1 or SOAP_1_2 |
trace(bool $trace = true) |
trace — keep the last request/response |
cache(int $mode) |
cache_wsdl — WSDL_CACHE_NONE/DISK/MEMORY/BOTH |
classMap(array $map) / classmap() |
classmap — WSDL type ⇒ PHP class (merged) |
typeMap(array $map) |
typemap (merged) |
header($ns, $name, $data, $mustUnderstand, $actor) |
Adds a \SoapHeader sent with every call |
header(SoapHeader $header) / customHeader() |
Adds a prebuilt \SoapHeader |
headers(iterable $headers) |
Adds several \SoapHeaders |
basicAuth(string $login, string $password) |
login, password, SOAP_AUTHENTICATION_BASIC |
digestAuth(string $login, string $password) |
login, password, SOAP_AUTHENTICATION_DIGEST |
certificate(string $path, ?string $passphrase) |
local_cert, passphrase — client certificate (mTLS) |
proxy(string $host, int $port, ?$login, ?$password) |
proxy_host, proxy_port, proxy_login, proxy_password |
connectionTimeout(int $seconds) |
connection_timeout |
userAgent(string $agent) |
user_agent |
compression(int $flags) |
compression, e.g. SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP |
keepAlive(bool $keepAlive = true) |
keep_alive |
features(int $flags) |
features, e.g. SOAP_SINGLE_ELEMENT_ARRAYS |
encoding(string $encoding) |
encoding |
streamContext(array|resource $context) |
stream_context — array is turned into a context |
verifySsl(bool $verify = true) |
false disables peer/host verification |
client(string $class) |
Use a custom \SoapClient subclass |
option(string $key, mixed $value) |
Any raw option |
options(array $options) |
Several raw options — these win over everything else |
exceptions is always true: failures are reported as exceptions, never as return values.
Read timeouts: PHP's SOAP extension reads responses using the
default_socket_timeoutini setting. Useini_set('default_socket_timeout', 60)for long-running operations.
The configured values are readable (but not writable) as properties, e.g.
Soap::service('Weather')->wsdl, ->trace, ->classMap, ->headers, and
->toOptions() returns the exact options array passed to \SoapClient.
Calling operations
$result = Soap::call('Service.Method', $arguments, $options, $inputHeaders, $outputHeaders);
| Parameter | Description |
|---|---|
$target |
"Service.Method". The last dot separates the method, so service names may contain dots (acme.billing.GetInvoice). |
$arguments |
Array passed to \SoapClient::__soapCall(). |
$options |
Per-call options: location, uri, soapaction. |
$inputHeaders |
\SoapHeader or array of headers for this call only. |
$outputHeaders |
Passed by reference, filled with the response headers. |
About $arguments: most WSDL services use the document/literal wrapped style, where an
operation takes a single parameter object. Pass that object as the only element of the array:
// document/literal: one wrapper parameter
Soap::call('Currency.GetConversionAmount', [
['CurrencyFrom' => 'USD', 'CurrencyTo' => 'EUR', 'RateDate' => '2026-09-30', 'Amount' => '1000'],
]);
// rpc style / non-WSDL: positional parameters
Soap::call('Calculator.add', [2, 3]);
Reading response headers requires a by-reference argument, which PHP cannot pass through a
facade — call the injected SoapWrapper instead:
use Siberfx\Soap\SoapWrapper;
public function transfer(SoapWrapper $soap, array $payload): mixed
{
$result = $soap->call('Bank.Transfer', [$payload], outputHeaders: $headers);
$sessionId = $headers['SessionId'] ?? null;
return $result;
}
Class maps
Map WSDL complex types to your own classes to get typed requests and responses:
namespace App\Soap\Types;
class GetConversionAmount
{
public function __construct(
public string $CurrencyFrom,
public string $CurrencyTo,
public string $RateDate,
public string $Amount,
) {}
}
class GetConversionAmountResponse
{
public string $GetConversionAmountResult;
}
Soap::add('Currency', fn (Service $s) => $s
->wsdl('https://currencyconverter.kowabunga.net/converter.asmx?WSDL')
->classMap([
'GetConversionAmount' => GetConversionAmount::class,
'GetConversionAmountResponse' => GetConversionAmountResponse::class,
])
);
/** @var GetConversionAmountResponse $response */
$response = Soap::call('Currency.GetConversionAmount', [
new GetConversionAmount('USD', 'EUR', '2026-09-30', '1000'),
]);
$response->GetConversionAmountResult;
SOAP headers
Headers added on the service are sent with every call:
Soap::add('Secure', fn (Service $s) => $s
->wsdl('https://example.com/secure?wsdl')
->header('http://example.com/auth', 'AuthHeader', [
'Username' => config('services.secure.user'),
'Password' => config('services.secure.password'),
], mustUnderstand: true)
);
Headers for a single call:
Soap::call('Secure.GetOrders', [$request], inputHeaders: [
new SoapHeader('http://example.com/tracing', 'CorrelationId', (string) Str::uuid()),
]);
Working with the client directly
Soap::client($name) returns the cached client — a Siberfx\Soap\SoapClient (an extended
\SoapClient) unless you configured another class:
$client = Soap::client('Currency');
$client->functions(); // operations described by the WSDL
$client->types(); // types described by the WSDL
$client->call('GetCurrencies');
$client->GetCurrencies(); // native magic calls work too
Pass a closure to receive the client and return a value:
$rates = Soap::client('Currency', fn (SoapClient $client) => $client->call('GetCurrencies'));
Calls made directly on the client bypass the wrapper, so they are not faked, recorded, converted into
SoapCallExceptions or reported through events. PreferSoap::call().
Debugging requests
Enable trace() on a service (or SOAP_TRACE=true for all of them) and inspect the raw XML:
Soap::call('Currency.GetCurrencies');
$client = Soap::client('Currency');
$client->lastRequest();
$client->lastRequestHeaders();
$client->lastResponse();
$client->lastResponseHeaders();
With trace enabled, the XML is also attached to SoapCallException and SoapResponseReceived.
Error handling
All package exceptions extend Siberfx\Soap\Exceptions\SoapException (a RuntimeException).
| Exception | Thrown when |
|---|---|
SoapCallException |
The server returned a SOAP fault or the transport failed during a call. |
ClientCreationException |
The client could not be built (unreachable / invalid WSDL, bad options). |
ServiceNotFoundException |
Calling a service that was not registered. |
ServiceAlreadyExistsException |
Registering a name that is already taken. |
InvalidServiceConfigurationException |
Missing WSDL / location+uri, or a malformed "Service.Method" target. |
StrayCallException |
A faked call had no stub while preventStrayCalls() is enabled. |
SoapCallException gives you everything about the failure:
use Siberfx\Soap\Exceptions\SoapCallException;
try {
Soap::call('Bank.Transfer', [$payload]);
} catch (SoapCallException $e) {
$e->service; // "Bank"
$e->method; // "Transfer"
$e->faultCode(); // e.g. "soap:Server"
$e->faultString(); // human readable message from the server
$e->detail(); // the <detail> element, if any
$e->lastRequest; // raw XML (trace enabled)
$e->lastResponse; // raw XML (trace enabled)
$e->fault; // the original \SoapFault (also getPrevious())
}
Events
When soap.events is enabled (default), every Soap::call() dispatches:
| Event | Properties |
|---|---|
Siberfx\Soap\Events\SoapRequestSending |
service, method, arguments |
Siberfx\Soap\Events\SoapResponseReceived |
service, method, arguments, response, duration (ms), lastRequest, lastResponse |
Siberfx\Soap\Events\SoapRequestFailed |
service, method, arguments, exception (SoapCallException), duration (ms) |
Example: log slow or failing calls.
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Siberfx\Soap\Events\SoapRequestFailed;
use Siberfx\Soap\Events\SoapResponseReceived;
Event::listen(function (SoapResponseReceived $event) {
if ($event->duration > 2000) {
Log::warning("Slow SOAP call {$event->service}.{$event->method}", ['ms' => $event->duration]);
}
});
Event::listen(function (SoapRequestFailed $event) {
Log::error($event->exception->getMessage(), [
'request' => $event->exception->lastRequest,
'response' => $event->exception->lastResponse,
]);
});
Arguments may contain credentials or personal data — scrub them before logging.
Testing
Call Soap::fake() and no request leaves your application. Every call is recorded so you can
assert on it. Services do not even need to be registered while faked.
use Siberfx\Soap\Facades\Soap;
use Siberfx\Soap\Testing\RecordedCall;
public function test_it_converts_currency(): void
{
Soap::fake([
'Currency.GetConversionAmount' => (object) ['GetConversionAmountResult' => '921.50'],
]);
$this->post('/convert', ['from' => 'USD', 'to' => 'EUR', 'amount' => 1000])
->assertOk()
->assertSee('921.50');
Soap::assertCalled('Currency.GetConversionAmount', fn (RecordedCall $call) =>
$call->argument('0.CurrencyFrom') === 'USD'
);
}
Stubs
| Stub key | Matches |
|---|---|
'Currency.GetRates' |
exactly that call (exact keys always win) |
'Currency.*' |
any method of the service |
'*' |
everything |
| Stub value | Result |
|---|---|
| any value | returned as the response |
Closure |
called with the RecordedCall; its return value is used |
\SoapFault |
converted to SoapCallException (just like a real fault) |
any other Throwable |
thrown as-is (e.g. simulate a timeout) |
Soap::fake([
'Weather.Forecast' => fn (RecordedCall $call) => "Sunny in {$call->argument('0.city')}",
'Bank.Transfer' => new SoapFault('Server', 'Insufficient funds'),
'Currency.*' => 1.08,
]);
Unmatched calls return null. To make them fail instead:
Soap::fake([...])->preventStrayCalls();
Soap::fake() can be called multiple times; stubs are merged.
Assertions
Soap::assertCalled('Currency.GetConversionAmount');
Soap::assertCalled('Currency.*', fn (RecordedCall $call) => $call->argument('0.Amount') === '1000');
Soap::assertNotCalled('Bank.Transfer');
Soap::assertCalledTimes('Currency.GetConversionAmount', 2);
Soap::assertNothingCalled();
Soap::recorded(); // list<RecordedCall>
Soap::recorded(fn (RecordedCall $call) => $call->service === 'Bank');
RecordedCall exposes service, method, arguments, target() ("Service.Method") and
argument($key, $default) which supports dot notation into arrays and objects.
Customisation
Custom client class
Extend Siberfx\Soap\SoapClient (or \SoapClient) — for example to sign requests with WS-Security:
use Siberfx\Soap\SoapClient;
class SignedSoapClient extends SoapClient
{
public function __doRequest(
string $request,
string $location,
string $action,
int $version,
bool $oneWay = false,
?string $uriParserClass = null, // PHP 8.5+, harmless on 8.4
): ?string {
$signed = app(RequestSigner::class)->sign($request);
return parent::__doRequest($signed, $location, $action, $version, $oneWay);
}
}
Soap::add('Government', fn (Service $s) => $s->wsdl('...')->client(SignedSoapClient::class));
Custom client factory
All clients are built by Siberfx\Soap\ClientFactory. Bind your own subclass to change how
every client is created:
$this->app->singleton(\Siberfx\Soap\ClientFactory::class, MyClientFactory::class);
Migrating from notfalsedev/laravel-soap
The API is intentionally familiar:
| notfalsedev/laravel-soap | siberfx/laravel-soap |
|---|---|
Artisaninweb\SoapWrapper\SoapWrapper |
Siberfx\Soap\SoapWrapper or the Soap facade |
$soapWrapper->add('Name', fn ($service) => ...) |
Soap::add('Name', fn (Service $service) => ...) |
$service->name('Name') |
not needed — the name is the first add() argument |
->wsdl(), ->trace(), ->classmap(), ->cache(), ->options(), ->certificate(), ->header(), ->customHeader() |
same names |
$soapWrapper->call('Name.Method', [...]) |
Soap::call('Name.Method', [...]) |
$soapWrapper->client('Name', fn ($client) => ...) |
Soap::client('Name', fn ($client) => ...) (returns the callback result) |
$client->call('Method', [...]) |
same |
raw SoapFault |
SoapCallException (original fault via ->fault) |
Development
composer install
composer test
The test suite runs real SOAP round-trips against an in-process SoapServer, so no network
access is required. The soap extension must be enabled.
Changelog
See CHANGELOG.md.
License
The MIT License (MIT). See LICENSE.md.