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

Soapy Laravel Package

sourcetoad/soapy

Soapy is a lightweight Laravel/PHP package that simplifies SOAP integrations. It offers a clean client wrapper for WSDL endpoints, requests, and responses, helping developers connect to legacy SOAP services without boilerplate or complex configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Soapy is a thin, fluent wrapper around PHP's native SoapClient that gives you a Laravel facade, a config file, and a chainable request builder. As of v3.1.0 it supports PHP 8.4 and Laravel 12; there are no API changes from v3.0.0, so this is a drop-in upgrade (composer update sourcetoad/soapy).

Minimum steps:

  1. Confirm the runtime prerequisite. The package requires the ext-soap PHP extension. It is not bundled with Laravel and is missing from many slim Docker images. Verify with php -m | grep soap before doing anything else — a missing extension produces confusing "class SoapClient not found" errors rather than a clear dependency message.
  2. Install and publish config.
    composer require sourcetoad/soapy
    php artisan vendor:publish --tag=soapy-config
    
    This gives you config/soapy.php (WSDL URL, trace, exceptions, timeouts, WSDL cache mode, stream_context, classmap/typemap).
  3. First call — smoke test against a real WSDL in php artisan tinker:
    use Sourcetoad\Soapy\Facades\Soapy;
    
    $response = Soapy::endpoint(config('soapy.wsdl'))
        ->call('SomeOperation')
        ->withArguments(['id' => 123])
        ->response();
    
    If the WSDL is unreachable or malformed, this throws at endpoint construction time, not at call time — so this single line validates connectivity, auth, and WSDL parsing all at once.

Where to look first: the published config/soapy.php (it is the primary integration surface), then the package README for the exact response accessors (asJson() / raw XML) available in your version. Keep 'trace' => true and 'exceptions' => true in local/staging from day one — they cost little and make every subsequent debugging session trivial.

First real use case: wrapping a single external SOAP operation (a lookup, a submission) behind a dedicated App\Services\* class that returns a typed DTO. Do not call the facade from controllers or jobs directly; see below.

Implementation Patterns

Config-driven, per-service endpoints. Don't let the facade be called with hardcoded URLs. Publish the config, then add one entry per upstream system:

// config/soapy.php
'connections' => [
    'billing' => [
        'wsdl' => env('BILLING_WSDL'),
        'trace' => env('APP_DEBUG', false),
        'exceptions' => true,
        'connection_timeout' => 10,
        'cache_wsdl' => env('APP_ENV') === 'production' ? WSDL_CACHE_BOTH : WSDL_CACHE_NONE,
    ],
],

Service wrapper + DTO boundary. The highest-value pattern: one class per remote system, constructor-injected config, returning your own DTOs — never the package's response object.

final class BillingClient
{
    public function charge(ChargeRequest $req): ChargeResult
    {
        $raw = Soapy::endpoint(config('soapy.connections.billing.wsdl'))
            ->call('Charge')
            ->withArguments([
                'account' => $req->account,
                'amount'  => $req->amountMinor / 100,
                'currency'=> $req->currency,
            ])
            ->response();

        return ChargeResult::fromArray((array) $raw);
    }
}

This gives you a seam for tests, one place to add retry/logging, and — importantly — insulation from Soapy's response shape if the package changes.

Typed argument objects over arrays. SOAP's wire format is namespace-sensitive; passing plain arrays works for simple operations but silently drops elements whose XSD type the encoder can't infer. For nested or xsd:any content, build SoapVar instances (or configure classmap in config/soapy.php) and pass those as arguments. When in doubt, echo the generated envelope before blaming the remote server.

Auth and SOAP headers. For WS-Security / API-key headers, prefer whatever header helper your version exposes on the builder; if the operation needs anything unusual, drop to the underlying client (->soapClient() or equivalent) and use native SoapHeader objects. Keep header construction in the wrapper class, keyed off config('services.*') — never inline credentials.

Retries and timeouts. SOAP calls are ordinary blocking I/O. Set connection_timeout/timeout in config and wrap writes in retry() only for idempotent operations — there is no generic idempotency key in SOAP, so blind retries on Create/Submit operations will double-post.

Queued jobs for anything slow. A flaky third-party SOAP endpoint should never sit in a web request. Dispatch a job, store the response, and poll — that also gives you a natural place for backoff and dead-letter handling.

Testing. ext-soap cannot be intercepted by Laravel's Http fake. Two reliable approaches: (a) bind a BillingClient interface in the container and swap in a fake, keeping all SOAP assertions out of your feature tests; or (b) record real request/response XML pairs into fixtures and have a thin transport double replay them for contract tests. Prefer (a) for the bulk of tests and reserve (b) for the one test that validates your argument mapping.

Gotchas and Tips

  • ext-soap is invisible until it isn't. Add ext-soap: * to your composer.json require block so CI fails at install time rather than at runtime.
  • trace defaults bite you. If trace is false, last-request/last-response inspection returns empty strings and you'll waste an hour thinking the request was never sent. Enable it in every non-production environment.
  • exceptions => false changes your error handling completely. Faults come back as SoapFault objects instead of throwing, so try/catch silently does nothing. Pick one mode per environment and keep it consistent; production throwing + a single catch at the wrapper boundary is usually cleanest.
  • WSDL caching causes phantom failures. After an upstream WSDL change, a cached copy keeps you talking to the old contract. WSDL_CACHE_NONE in dev/staging, WSDL_CACHE_BOTH in production, and remember to clear after deploys that change config.
  • php artisan config:cache freezes your endpoints. Any env() call must live inside config/soapy.php, not in your service class — otherwise it returns null the moment config is cached in production.
  • Namespace mismatches look like "looks fine, returns empty." If the server returns an empty body or a generic fault with no detail, the argument namespace is almost always the cause. Inspect getLastRequest() (with trace on) and compare the element prefixes against the WSDL's targetNamespace.
  • Nested arrays need one more level. PHP's encoder often expects ['items' => ['item' => [...]]] rather than ['items' => [...]]. If only the first element arrives server-side, that's the reason.
  • null is not "omit". Passing null explicitly serialises an empty element; to omit a field entirely you must leave the key out of the argument array. Some validators reject the former.
  • Large payloads and memory. Deeply nested responses become enormous stdClass graphs. Request only the fields you need where the operation supports filtering, and avoid logging full responses in production.
  • TLS/proxy/client-cert setups belong in stream_context, not in code. Configure ssl/http context options in config/soapy.php so certificates and proxy settings aren't scattered across service classes.
  • Logging. Log the operation name, duration, and outcome at the wrapper boundary — never the raw envelope (it contains credentials and PII). Gate full-envelope logging behind APP_DEBUG.
  • Upgrade note for v3.1.0. The PHP 8.4 and Laravel 12 support is additive with no deprecations or breaking changes, but if you skipped v3.0.0, review that changelog too — v3 is the line where framework and PHP minimums were raised, so older Laravel 9/PHP 8.0 apps must stay on the 2.x series.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource