Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Engine Laravel Package

php-soap/engine

A modern SOAP engine for PHP, offering a robust foundation for building SOAP clients and servers with WSDL handling, type mapping, and reliable interoperability for enterprise and legacy web services.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the engine plus a transport — the engine itself has no HTTP opinions, so out of the box you get nothing that can actually talk to a server:

composer require php-soap/engine php-soap/psr18-transport
# plus any PSR-18 client + PSR-17 factories you already use
composer require symfony/http-client nyholm/psr7

The fastest path to a first call is the convenience builder, which wires metadata, encoder, decoder and transport together from a WSDL URL:

use Http\Discovery\Psr18ClientDiscovery;
use PhpSoap\Engine\SimpleBuilder;

$client = SimpleBuilder::for('https://example.com/service?wsdl')
    ->withPsr18Client(Psr18ClientDiscovery::find())
    ->build();

$result = $client->request('GetWeather', ['City' => 'Ghent']);

If you need explicit control (custom middleware, custom encoder), assemble it by hand — the engine is just Engine(driver, encoder, decoder) wrapped by a Client that also holds the WSDL Metadata. Same object graph, more knobs.

Where to look first, in this order:

  1. src/Engine.php — the orchestrator. One request(string $operation, array $arguments) method; everything else is plumbing around it.
  2. src/Client/ — the facade you'll actually inject into your services.
  3. src/Transport/Transport.php — the seam between "SOAP" and "HTTP". This is your testing hook.
  4. src/Encoder.php / src/Decoder.php — array ↔ XML. Read these when a payload looks wrong.
  5. src/Middleware/ — the pipeline; where cross-cutting behaviour belongs.
  6. src/Metadata/ — what the engine extracted from the WSDL (operations, types, bindings). Since 2.21 the TypeCollection can be looked up by name and XML namespace (findByNameAndXmlNamespace()), which is the lookup you want whenever the WSDL reuses local type names across namespaces.

First use case: pick one legacy ext-soap SoapClient in your codebase — ideally a document/literal wrapped service — and port that single integration. You get a real WSDL, real faults, and a known-good response to diff against. Don't start with the gnarliest RPC/encoded service in the estate; port that one second, once you know the debugging loop.

Implementation Patterns

Wrap the engine per service, don't scatter request() calls. The engine's API is generic (operation + positional array). That is exactly the kind of thing you don't want leaking into controllers. Create one thin class per WSDL that owns the operation names and argument shapes:

final class WeatherService
{
    public function __construct(private Client $client) {}

    public function getWeather(string $city): Weather
    {
        return Weather::fromArray(
            $this->client->request('GetWeather', ['City' => $city])
        );
    }
}

Bind that class in the container as a singleton. Building the engine re-parses WSDL metadata, so it should happen once per process, not once per call.

Map the response into domain objects immediately. The decoder gives you arrays (or stdClass depending on configuration). Treat that as raw input: cast numerics, normalize date strings, flatten the _/attributes conventions, and reject unknown shapes loudly. This is the layer that stops "SOAP array soup" propagating through your app.

Use middleware for everything cross-cutting. This is where the package earns its keep over ext-soap:

  • Request/response logging (dump the raw envelope — you'll need it constantly).
  • Correlation IDs and SOAPAction/Content-Type fixups.
  • Basic auth and custom headers.
  • Timing metrics and fault-to-domain-exception translation.
  • Retry for idempotent operations — but only there.

Middleware compose in a collection, so keep each one single-purpose and order them deliberately (auth before logging if you want the header in the log, etc.).

Swap the transport for tests. Because Transport is an interface, you can replay recorded SOAP responses without touching HTTP. The pattern that pays off: commit one WSDL file and a folder of request → response XML fixtures per service. Then your whole SOAP suite runs offline, deterministically, in CI — the single biggest quality-of-life improvement over ext-soap, which requires a live endpoint or fragile mocking.

final class FixtureTransport implements Transport
{
    public function request(SoapRequest $request): SoapResponse
    {
        return SoapResponse::fromFile(__DIR__ . "/fixtures/{$this->next()}.xml");
    }
}

Keep SOAP 1.1 vs 1.2 a configuration concern. The envelope namespace, Content-Type and SOAPAction handling differ. Pick the binding in config, not with an if in your service class — most servers accept one and reject the other with an unhelpful 500.

Cache the parsed WSDL. Parsing is the slow, network-bound part. In Laravel, cache the metadata (or the built Client) in Redis/file cache keyed by WSDL URL + a content hash, and rebuild on deploy. Refresh it out-of-band if the provider changes their WSDL, because otherwise you'll chase phantom "operation not found" errors.

When you write a custom encoder/decoder, resolve types through the namespace-aware collection. If you need to inspect the WSDL type graph (optional elements, xsi:nil handling, generating a probe payload), reach for findByNameAndXmlNamespace() rather than findByName(). Enterprise WSDLs routinely define Header, Result or Item in several namespaces; name-only lookup returns the wrong type and the resulting validation fault points at the server, not at your bug.

Gotchas and Tips

  • Arguments are ordered, not named. Passing ['City' => $city] works only because the SDK maps it onto the WSDL sequence; if the server has overloaded or reordered elements, you get a validation fault rather than a PHP error. When in doubt, pass positional arrays matching the WSDL order.
  • nil and attributes are the sharp edges. xsi:nil="true", qualified vs unqualified elements, and attributes encoded as @/_ keys are where hand-built payloads silently break. Encode a probe request once, capture the XML, and keep it as a fixture before you write any real code.
  • Name collisions across namespaces are the classic type bug. The same local name in two namespaces is legal and common. On 2.21+ use TypeCollection::findByNameAndXmlNamespace(); on older versions you're stuck doing an O(n) filter over the collection yourself, so if you're doing type introspection, this is a reason to bump.
  • Document/literal wrapped is the happy path. RPC/encoded, multi-part messages, and MTOM attachments range from awkward to unsupported. Check your WSDL's binding style before committing to a migration; if it's RPC/encoded, budget time or stay on ext-soap.
  • No WS-Security built in. Username tokens, X.509 signing and timestamps are middleware your team writes (or a separate package). Plan for it — it's the most common reason a "simple" port stalls.
  • Faults are HTTP responses, not exceptions. A SOAP fault typically arrives as HTTP 500 with a valid XML body. Don't let your PSR-18 client's "throw on 4xx/5xx" behaviour swallow the fault detail — decode the body first, then throw a domain exception carrying the faultcode/faultstring.
  • Debug by dumping envelopes, not by reading stack traces. Log the exact request XML and the raw response body. Nine out of ten "it doesn't work" cases are a namespace, a SOAPAction value, or an auth header — all visible only in the XML.
  • Timeouts and retries live in the PSR-18 client, not the engine. Configure them there and keep the engine ignorant. The engine will happily wait forever.
  • Everything decodes as strings. Decimals, booleans ("true"/"1"), and dates all arrive as text. Cast at the boundary; don't rely on PHP's loose comparison downstream.
  • Watch response size. A large result set becomes one big in-memory string before decoding. If you're pulling 50 MB reports, stream or paginate at the operation level — the engine has no streaming story.
  • Pin the php-soap/* family together. The engine, transport and WSDL packages move in lockstep; mixing minor versions produces confusing type errors at construction time. 2.21.0 is additive (a new metadata lookup helper, no signature or behaviour changes elsewhere), so it's a safe bump on its own — just move the whole family, not one package.
  • Extension points worth knowing: custom Encoder/Decoder for provider-specific quirks, a custom Transport for non-PSR-18 setups (or fixtures), middleware for anything that touches every call, and the metadata TypeCollection for introspecting the WSDL. You rarely need to fork; you almost always need one middleware.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console