burti/besimple-soap-common
A shared foundation for BeSimple SOAP client and server packages, supplying common classes, WSDL utilities, and type definitions. Simplifies building and consuming SOAP services in PHP and Laravel applications by keeping reusable SOAP components consistent across integrations.
This is not a Laravel package. It is the shared low‑level library from the old Symfony2-era BeSimpleSoap suite (SOAP client/server classes, MIME/MTOM attachment handling, WSDL cache, type converters). There is no service provider, no config file, no facade, and no auto-discovery. You consume it as a plain Composer dependency and wire it yourself.
Prerequisites and install
php -m | grep -i soap # ext-soap is mandatory
composer require burti/besimple-soap-common
composer dump-autoload -o
Because the package is untagged/old in most forks, you may need an explicit constraint (^0.3 or a dev-* branch) and possibly "minimum-stability": "dev". Run the install on the exact PHP version you deploy to before committing — old PSR-0 code frequently trips PHP 8.x deprecations.
Where to look first — after install, read the source rather than the README:
vendor/burti/besimple-soap-common/src/BeSimple/SoapCommon/SoapClient.php — drop-in \SoapClient subclass with extra options.../SoapServer.php — \SoapServer subclass.../Mime/Part.php, .../Mime/MultiPart.php, .../Mime/Parser.php — MTOM/XOP attachments.../Helper.php — option key constants.../Cache.php — WSDL cache directory handling.../Converter/ — custom XSD type convertersFirst use case — centralize SOAP configuration and call a third-party service with attachments or a non-standard encoding. Bind a configured client in a service provider:
// config/soap.php
return [
'billing' => [
'wsdl' => env('BILLING_WSDL'),
'options' => [
'soap_version' => SOAP_1_2,
'cache_wsdl' => WSDL_CACHE_NONE,
'connection_timeout' => 10,
'exceptions' => true,
'trace' => env('SOAP_TRACE', false),
],
],
];
// app/Providers/SoapServiceProvider.php
public function register(): void
{
$this->app->singleton('soap.billing', function ($app) {
$cfg = $app['config']['soap.billing'];
return new \BeSimple\SoapCommon\SoapClient($cfg['wsdl'], $cfg['options']);
});
}
Then app('soap.billing')->__soapCall('GetInvoice', [$args]);. Note this is the Common package only — WS-Security, WS-Addressing, and WSDL generation live in sibling BeSimple packages that may not be installable on modern PHP.
Wrap the client behind your own interface. Never type-hint the BeSimple class in application code — you want to be able to swap it for phpro/soap-client, laminas-soap, or raw \SoapClient later.
interface BillingGateway { public function invoice(string $id): Invoice; }
class BeSimpleBillingGateway implements BillingGateway
{
public function __construct(private \BeSimple\SoapCommon\SoapClient $client) {}
public function invoice(string $id): Invoice
{
try {
$res = $this->client->__soapCall('GetInvoice', [['id' => $id]]);
} catch (\SoapFault $e) {
throw new BillingUnavailable($e->getMessage(), previous: $e);
}
return Invoice::fromSoap($res);
}
}
Typed responses via classmap. BeSimple's client forwards standard options, so generated value objects work exactly as with native SOAP:
'classmap' => [
'Invoice' => \App\Soap\Types\Invoice::class,
'InvoiceLine' => \App\Soap\Types\InvoiceLine::class,
],
MTOM attachments. Build a Mime\Part per binary, attach with MultiPart, and match Content-ID to the cid: reference inside your XML body. On responses, feed the raw response body to Mime\Parser and hydrate Part objects by Content-ID. Verify the exact constructor argument order against Part.php — it moves between forks.
Server endpoint. SoapServer writes to output; buffer it in a controller:
public function __invoke(Request $request, \BeSimple\SoapCommon\SoapServer $server)
{
$server->setObject(new OrdersHandler());
ob_start();
try {
$server->handle($request->getContent());
} catch (\SoapFault $e) {
// SoapServer already emitted a fault envelope
}
return response(ob_get_clean(), 200, ['Content-Type' => 'text/xml; charset=utf-8']);
}
Bind SoapServer per service with the correct WSDL and uri options, and disable Laravel's CSRF/VerifyCsrfToken for that route.
Logging / tracing. The idiomatic extension point is subclassing and overriding __doRequest (or __doResponse), not middleware:
class LoggingSoapClient extends \BeSimple\SoapCommon\SoapClient
{
public function __construct($wsdl, array $options, private LoggerInterface $log)
{ parent::__construct($wsdl, $options); }
public function __doRequest($request, $location, $action, $version, $oneWay = false): ?string
{
$this->log->debug('soap.req', ['location' => $location, 'xml' => $request]);
$r = parent::__doRequest($request, $location, $action, $version, $oneWay);
$this->log->debug('soap.res', ['xml' => (string) $r]);
return $r;
}
}
Register it in the container instead of the base class. Same hook is where you'd insert XML-DSig signing if you add robrichards/wse-php.
Testing. Because the transport is ext-soap streams, you can't Http::fake() it. Test your BillingGateway consumers against a fake implementation of your interface, and keep one integration test that hits a recorded WSDL/fixture (e.g. via soap.wsdl_cache_enabled=0 and a local .wsdl).
composer.lock, mirror the fork in your VCS, and keep the adapter interface so replacing it is a one-file change.BeSimple\SoapCommon\...; Composer handles it, but opcache/preloading setups sometimes miss PSR-0 paths. composer dump-autoload -o after every deploy.__doRequest signature mismatches, and each()/create_function() remnants in older tags. Pin to the newest fork commit and silence deprecations only after reading each one. PHP's built-in \SoapClient::__doRequest now requires ?string return and bool $oneWay — match it exactly.Cache:: writes under a configured directory; point it at storage/framework/cache/soap and ensure it is writable by the PHP-FPM user. On multi-node deployments use a shared volume or WSDL_CACHE_NONE, or every request re-fetches the WSDL over the network.trace => true in production is expensive and PII-leaky. Gate it by env and route the log channel to a redacted/rotated sink.stream_context with ssl cafile/verify_peer, and set HTTP_PROXY env for outbound calls; failures surface as generic SoapFault: Could not connect to host.Content-Transfer-Encoding (base64 vs binary) on both directions; decode base64 parts before persisting; watch memory on large MTOMs — the whole envelope is in memory, so cap memory_limit accordingly and stream to a temp file if needed.exceptions => true gives SoapFault; false returns a SoapFault object that is easy to accidentally treat as a result. Wrap __soapCall in your gateway and normalize.phpro/soap-client + wse-php instead of stitching the old suite together.Mime\Part factories for attachment serialization, Converter\AbstractConverter subclasses for xsd:dateTime/base64Binary, Helper:: option constants instead of magic strings, and __doRequest overrides for logging/signing/metrics. Everything else should stay in Laravel: config, container, jobs, retries, and an adapter that hides the library.How can I help you explore Laravel packages today?