manoakys/nusoap-bundle
A Symfony bundle that wraps the NuSOAP library, letting you build SOAP servers and clients from your Symfony application with service configuration handled through the bundle rather than manual NuSOAP setup.
Reality check before you type composer require: this is a Symfony2 bundle, not a Laravel package. It ships no ServiceProvider, no config/*.php, no facade, and no auto-discovery. The thing that actually does work is the underlying nusoap/nusoap library it pulls in. Treat the bundle as a reference implementation, and wire NuSOAP yourself.
Step 1 — probe Composer, don't fight it.
composer require manoakys/nusoap-bundle
On Laravel 9+ this will almost certainly die on a constraint conflict, because the bundle wants symfony/framework-bundle ^2.x while your app pulls Symfony 6/7 components transitively. Read the error once, then move on. Do not reach for --ignore-platform-reqs to force it — you'll get an inert bundle plus a dependency graph Composer can no longer reason about.
Step 2 — install the library, not the bundle.
composer require nusoap/nusoap
If that is unavailable or you need a pinned legacy revision, vendor the lib/ folder into app/Support/Nusoap/ and add a classmap autoload entry in composer.json. Either way, verify \nusoap_client and \nusoap_server resolve before writing any business logic:
php artisan tinker --execute="var_dump(class_exists('nusoap_client'));"
Step 3 — where to look first. The NuSOAP source, not the bundle. Every behaviour you will debug — encoding, fault handling, WSDL parsing, transport — lives in nusoap_client, nusoap_server, wsdl, and nusoap_base. The bundle is a handful of YAML and a Bundle class; read it once in five minutes and you have read all of it.
Step 4 — first use case. You have a partner WSDL that ext-soap rejects (rpc/encoded, malformed schema, exotic types), or you need to expose a WSDL without the SOAP extension compiled in. Start with the client, behind an interface, bound in a provider.
// app/Contracts/SoapGateway.php
namespace App\Contracts;
interface SoapGateway
{
public function call(string $operation, array $params = []): mixed;
}
// app/Services/Soap/NusoapGateway.php
namespace App\Services\Soap;
use App\Contracts\SoapGateway;
use RuntimeException;
class NusoapGateway implements SoapGateway
{
protected \nusoap_client $client;
public function __construct(string $wsdl, array $options = [])
{
$this->client = new \nusoap_client($wsdl, $options['wsdl_mode'] ?? 'wsdl');
$this->client->soap_defencoding = $options['encoding'] ?? 'UTF-8';
$this->client->decode_utf8 = $options['decode_utf8'] ?? false;
$this->client->timeout = $options['timeout'] ?? 30;
$this->client->response_timeout = $options['response_timeout'] ?? 60;
if ($err = $this->client->getError()) {
throw new RuntimeException("NuSOAP init failed: {$err}");
}
}
public function call(string $operation, array $params = []): mixed
{
$result = $this->client->call($operation, $params);
if ($this->client->fault) {
throw new RuntimeException(sprintf(
'SOAP fault [%s] %s',
$this->client->faultcode,
$this->client->faultstring
));
}
if ($err = $this->client->getError()) {
throw new RuntimeException("SOAP transport error: {$err}");
}
return $result;
}
}
// app/Providers/SoapServiceProvider.php
public function register(): void
{
$this->app->bind(SoapGateway::class, function () {
return new NusoapGateway(
config('soap.endpoints.legacy.wsdl'),
config('soap.endpoints.legacy')
);
});
}
That is essentially the entire value the Symfony bundle was offering, translated to a stack that will actually load it.
Config-driven multi-endpoint factory. You will rarely have one SOAP endpoint. Keep config/soap.php declarative and build named gateways from it, so a new partner is a config line, not a new class.
// config/soap.php
return [
'endpoints' => [
'billing' => [
'wsdl' => env('SOAP_BILLING_WSDL'),
'timeout' => 30,
'encoding' => 'UTF-8',
],
'legacy_erp' => [
'wsdl' => env('SOAP_ERP_WSDL'),
'wsdl_mode' => 'wsdl', // or 'no_wsdl'
'timeout' => 60,
],
],
];
Bind a small factory that caches resolved gateways per-request by name. Do not make it a singleton with static state across jobs (see Gotchas).
Expose a SOAP service from a route. NuSOAP's server echoes its response, which fights Laravel's response pipeline. Capture output and hand it back as a real response with the correct content type, and exclude the route from CSRF and from any middleware that might rewrite the body.
// routes/web.php
Route::post('/soap/orders', [SoapController::class, 'orders'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
public function orders(Request $request)
{
$server = new \nusoap_server();
$server->configureWSDL('ordersService', 'urn:orders');
$server->wsdl->schemaTargetNamespace = 'urn:orders';
$server->register(
'getOrder',
['orderId' => 'xsd:string'],
['return' => 'xsd:string'],
'urn:orders',
'urn:orders#getOrder',
'rpc',
'encoded',
'Look up an order by ID'
);
$payload = $request->getContent();
ob_start();
$server->service($payload);
$xml = ob_get_clean();
return response($xml, 200)->header('Content-Type', 'text/xml; charset=UTF-8');
}
Non-WSDL mode for broken partners. This is the one thing NuSOAP still does better than ext-soap. When the partner's WSDL is unusable but the endpoint works, construct with no_wsdl and hand-build the envelope.
$client = new NusoapGateway($endpointUrl, ['wsdl_mode' => 'no_wsdl']);
$client->call('getOrder', [
'orderId' => ['id' => $id, 'xmlns' => 'urn:orders'],
]);
Push every call into a queued job. SOAP here is blocking I/O with no async story and no integration with Laravel's Http retry/timeout abstractions. A dead counterparty will pin your web workers.
class SyncOrderToPartner implements ShouldQueue
{
public $tries = 3;
public $backoff = [10, 60, 300];
public $timeout = 120;
public function __construct(public int $orderId) {}
public function handle(SoapGateway $gateway): void
{
$gateway->call('getOrder', ['orderId' => (string) $this->orderId]);
}
public function retryUntil() { return now()->addHours(6); }
}
Contract tests against real payloads. Because there is no upstream, your test suite is the compatibility net. Check in the partner's WSDL and recorded request/response XML fixtures, and assert on structure — not just "no exception thrown."
it('parses a legacy getOrder response', function () {
$gateway = new NusoapGateway(__DIR__.'/fixtures/orders.wsdl');
// Point at a local fixture server or stub transport, then:
$result = $gateway->call('getOrder', ['orderId' => 'A100']);
expect($result)->toHaveKey('status')->and($result['status'])->toBe('SHIPPED');
});
Run these on every PHP minor upgrade. That is your only early warning.
WSDL disk caching. NuSOAP re-parses the WSDL on every instantiation, and that parse is slow (hundreds of ms for a large schema). Fetch the WSDL once, cache it to storage/app/soap/*.wsdl with a TTL, and construct the client against the local file path. NuSOAP accepts a filesystem path in place of a URL.
PHP 8 will bite you. NuSOAP relies on each(), removed in PHP 8.0 — instantiating the client on 8.x can fatal outright depending on the code path. There is no patch coming. If you must run it, audit every entry point with php -l and a smoke test, and expect dynamic-property deprecation noise on 8.2+. Pin your PHP version, or fork the offending lines.
fault and getError() are different failures. $client->fault is a SOAP-level fault from the server (a real business error). $client->getError() is a transport/parse failure (timeout, DNS, malformed XML). Checking only one is the single most common bug — you get silent false returns. Always check fault after call(), and getError() after both construction and call().
call() returns false on transport failure, not an exception. Guard the return type before indexing into it. A false propagating into $result['status'] produces a confusing "trying to access array offset on bool" three layers away from the real problem.
Encoding mismatches are silent. Set soap_defencoding = 'UTF-8' and decode_utf8 = false early and consistently. Leave them defaulted and non-ASCII characters in addresses/names arrive mangled or trigger a parse fault with no useful message. If the partner is ISO-8859-1, set it explicitly rather than letting the library guess.
The server echoes; it does not return. $server->service() writes to stdout. Without ob_start()/ob_get_clean() you get XML sprayed into the middle of your framework's output. Also strip any middleware that adds headers, compresses, or reformats the body on that route.
Raw request body, not $request->all(). NuSOAP wants the literal XML. Use $request->getContent(). Passing the parsed array produces a cryptic "no method" or empty-response fault.
No WSDL or response caching built in. Do it yourself, as above. But cache the schema, not the responses — caching a getOrder result across requests will hand one customer another customer's data.
Static state makes this unsafe under Octane and long-lived queue workers. NuSOAP and the class-level caches it accumulates are not designed for recycled processes. Either config('octane.warm') exclude these routes, restart workers aggressively (--max-jobs low), or run the SOAP client in a short-lived process/queue. Memory creep over hundreds of jobs is real.
TLS and XXE defaults are dated. NuSOAP predates modern transport hardening. Do not assume certificate verification is on. Where you control the endpoint, force it; where you don't, at minimum pin the expected host and audit what curl options the client sets. There is no CVE channel for this library — you are the CVE channel.
Debugging: dump the raw envelope. When a call fails opaquely, the fastest path is $client->request and $client->response (raw XML strings on the client object) plus $client->debug_str. Log those — never log them into a user-facing response, they contain credentials and payloads.
xsd:date/xsd:dateTime are not Carbon. NuSOAP serializes whatever string you give it. Passing a Carbon instance coerces via __toString() into a format the partner may reject. Format explicitly: $date->format('Y-m-d\TH:i:s').
Extension points are limited to what you build. There is no event system, no middleware, no plugin contract. The realistic seam is your SoapGateway interface plus a decorating gateway for logging/metrics. Keep business logic importing the interface only, so the eventual migration to ext-soap or a PSR-18 client is a one-line container binding, not a refactor.
Licensing. NuSOAP is LGPL. Confirm your obligations before shipping it inside a distributed product — linking and notice requirements apply, and that is a legal question, not a Composer one.
Own it explicitly. Zero stars, zero dependents, no maintainer. Pin the exact commit, vendor it if you can, and put a named owner and a migration ticket in your risk register before the first endpoint goes live. The exit plan is the deliverable, not the integration.
How can I help you explore Laravel packages today?