ricorocks-digital-agency/soap
A Laravel SOAP client with an expressive, HTTP-client-like API. Build SOAP clients, send requests, inspect responses, and fake SOAP calls in tests for fast, reliable integration testing.
Install the package with Composer and note the new major version constraint:
composer require ricorocks-digital-agency/soap:^4.0
The 4.0.0 release is a framework-support release: it adds Laravel 13 support and drops Laravel 11. That means:
^4.0.^3.0 or upgrade the framework first. Check your composer.json platform requirements (PHP version) alongside the Laravel bump before running composer update.Publish the package config if you want to tune defaults:
php artisan vendor:publish --tag=soap
Where to look first:
config/soap.php for the facade entry point and available options.RicorocksDigitalAgency\Soap\Facades\Soap and the underlying RicorocksDigitalAgency\Soap\Soap class — that is where to(), call(), hooks and faking live.First use case — a smoke test against a real WSDL:
use RicorocksDigitalAgency\Soap\Facades\Soap;
$response = Soap::to('https://example.com/service?wsdl')
->call('GetCustomer', ['id' => 123]);
dump($response->response); // raw decoded payload
Once that returns, wrap it in a small service class (see below) rather than scattering facade calls through controllers.
Requires the PHP ext-soap extension. Confirm with php -m | grep soap on every environment, including CI and production containers.
Wrap each remote service in a dedicated class. Keep the facade out of controllers, jobs and models. Inject it or resolve it inside a thin client:
namespace App\Services\Billing;
use RicorocksDigitalAgency\Soap\Facades\Soap;
class BillingSoapClient
{
public function __construct(private string $wsdl) {}
public function getInvoice(int $id): array
{
return Soap::to($this->wsdl)
->withOptions(['connection_timeout' => 10, 'cache_wsdl' => WSDL_CACHE_BOTH])
->call('GetInvoice', ['invoiceId' => $id])
->response;
}
}
Bind the class in a service provider so the WSDL URL and options come from config, not hardcoded strings.
Use hooks for cross-cutting concerns instead of repeating them. The package exposes request/response lifecycle hooks, which is the natural place for auth headers, correlation IDs, tracing and logging:
Soap::beforeRequest(function ($request) {
$request->arguments['authToken'] = config('services.billing.token');
return $request;
});
Register global hooks in a service provider (boot()), and request-scoped tweaks via withOptions() on the individual call.
Map SOAP faults to your own exception hierarchy. SOAP faults surface as PHP SoapFault exceptions; catch them at the client boundary and rethrow a domain exception so callers never depend on SOAP semantics:
try {
return $client->getInvoice($id);
} catch (\SoapFault $e) {
throw new BillingUnavailableException($e->getMessage(), previous: $e);
}
Decide per-endpoint whether you need WSDL mode. WSDL mode gives you functions() and argument validation but adds a fetch and parse cost on first call (mitigated by cache_wsdl). Non-WSDL mode (passing null plus location/uri options) is faster and more predictable when the remote WSDL is flaky or only reachable from inside a VPC.
Fake aggressively in tests. This is the biggest day-to-day win over raw SoapClient:
use RicorocksDigitalAgency\Soap\Facades\Soap;
Soap::fake(['*' => Soap::response(['status' => 'ok'])]);
// ... exercise the code under test ...
Soap::assertSent(fn ($request) => $request->method === 'GetInvoice');
Soap::assertNotSent(fn ($request) => $request->method === 'VoidInvoice');
Fake the client in feature tests, and reserve real calls for a small number of contract/integration tests tagged and excluded from the default suite. Assert on the service class behaviour in unit tests and on the outgoing request shape in feature tests — the assertions on method name and arguments are what catch regressions when the remote API changes.
Wrap write operations in jobs. SOAP calls are slow and synchronous. Push them onto the queue with retries and a backoff, then keep the HTTP request path read-only.
Expose typed return objects. The raw ->response is an anonymous object/array depending on the endpoint; normalise it into a DTO inside the client so the rest of the app is insulated from WSDL quirks.
^3.0 vs ^4.0) rather than relying on a single monorepo constraint. Also re-check the PHP requirement that comes with the Laravel 13 bump; CI images pinned to older PHP will fail at composer install before any code runs.ext-soap is a hard runtime dependency. It is easy to have it locally and miss it in a slim production image. Add ext-soap to composer.json require if the package doesn't already enforce it, so the failure happens at build time.register() — it will break artisan commands, queue workers and deploy scripts whenever the remote host is unreachable. Lazy-resolve inside the client method.cache_wsdl in withOptions(); otherwise every PHP process pays the fetch/parse cost. Be aware that cached WSDL can go stale after a remote schema change — clear it (or set WSDL_CACHE_NONE in a debug path) when you see "method not found" style errors that don't match the remote docs.tearDown() (or use the package's fake/reset helpers) or you will get order-dependent test failures that appear only when the full suite runs.connection_timeout and the PHP default_socket_timeout ini value both matter. Set explicit timeouts in options; the default can hang a queue worker for a long time on a dead endpoint.verify_peer/allow_self_signed stream context options. Never disable verification in production code paths — gate it on app()->isLocal().soap_version => SOAP_1_2; the default 1.1 envelope can be rejected with an unhelpful parse error.var_dump of the response object hides the actual fault string. Redact credentials from the logged envelope.Soap class (or bind your own implementation into the container behind the facade) instead of forking. That keeps future major-version upgrades — like this one — to a single composer update.How can I help you explore Laravel packages today?