wsdltophp/packagegenerator
wsdltophp/packagegenerator turns WSDL files and XML schemas into complete PHP packages, generating classes, models, and SOAP client code so you can integrate web services quickly without hand-writing repetitive boilerplate.
Install the generator as a dev dependency (it emits code you commit or build in CI; it isn't a runtime dependency):
composer require --dev wsdltophp/packagegenerator
php -d memory_limit=-1 vendor/bin/wsdltophp generate:package \
--urlorpath="https://example.com/service?wsdl" \
--destination="app/Services/BillingApi" \
--composer-name="acme/billing-api-client" \
--namespace="App\\Services\\BillingApi\\"
Where to look first:
vendor/bin/wsdltophp generate:package --help — options drift between releases; trust --help over blog posts.Struct/, EnumType/, ServiceType/, and Client.php (or Service class) directories — this is the actual API surface you'll code against.README.md in the output directory — it documents generated classes and typical usage per WSDL.First use case: Pick one operation (e.g., GetCustomer) and one response struct. Instantiate the generated client, call the operation, dump ->getResult(), and confirm the returned object graph before scaling up. Do not try to wire all operations at once — WSDLs with 200+ types will bury you.
Prerequisites: ext-soap must be enabled (this is the single most common "it doesn't work" cause). PHP 8.x recommended; check the composer constraint for exact minimum.
Commit generated code, don't regenerate per deploy. The output is deterministic given a WSDL, but WSDLs change without notice on many providers. Commit the generated tree, review the diff on regeneration, and treat regeneration as a deliberate, reviewed change.
Wrap the generated client in a Laravel service. Never call the generated Client directly from controllers or jobs. Wrap it:
// app/Services/BillingApi/BillingGateway.php
final class BillingGateway
{
public function __construct(private \App\Services\BillingApi\ServiceType\Billing $client) {}
public function getCustomer(string $id): ?Customer
{
try {
$resp = $this->client->getCustomer(new GetCustomer($id));
return $resp->getResult();
} catch (\SoapFault $e) {
throw new BillingApiException($e->getMessage(), previous: $e);
}
}
}
Bind it in a service provider and inject the interface into jobs/listeners.
Map generated structs to your domain at the boundary. The generated classes are a transport concern. Convert a generated Customer struct into your own DTO or Eloquent model in the gateway, so the rest of your app never depends on the vendor namespace. This also insulates you if the WSDL changes.
Put credentials/options in config/services.php and pass them to the generated client's constructor (login, password, trace, connection_timeout, etc.). The generated client exposes the standard SoapClient options array — reuse it.
Queue long calls. SOAP calls are synchronous and often slow. Dispatch a job (ShouldQueue, $timeout set) that resolves the gateway from the container. Use $this->release() with backoff for transient failures.
Test against a fake gateway, not a fake SoapClient. Because the gateway interface is yours, you mock it in feature tests. For integration tests against a recorded WSDL, point the client at a local .wsdl file (see gotchas) with wsdl_cache disabled.
CI regeneration check (optional but valuable): a scheduled job regenerates into a temp dir and diffs against the committed tree. Any diff means the upstream WSDL changed and needs review.
ext-soap is required and not always present. Many Docker/CI images ship without it. Add docker-php-ext-install soap or apt-get install php-soap. Check in a health/console command if the integration is core.SoapClient will happily hang forever. Always set connection_timeout (and default_socket_timeout via ini_set if needed). Wrap calls in a queue with a hard timeout.wsdl_cache options or fetch a copy of the WSDL to storage/app/wsdls/ and generate/client against the local file. This makes generation reproducible and insulates you from the vendor being down mid-deploy.--composer-name and autoloading. If you generate inside app/, the classes are autoloaded by your app's PSR-4 — do not add a second autoload entry. If you generate into a separate packages/ directory, add the PSR-4 mapping explicitly.phpstan.neon excludePaths and to .gitattributes/formatter ignores. Do run the formatter/linter on your wrappers, not the generated tree.Request, Response, Header). Always reference generated classes fully-qualified or via the wrappers — never use them at the top of a file that also touches Laravel types.__soapCall headers manually. Check generated classes for a setSoapHeader/__setSoapHeaders path or subclass the client and override __doRequest.EnumType\* classes. Be explicit about which you're passing — passing a raw string where an enum object is expected yields confusing SoapFaults from the server, not the client.null for fields absent in the response. Guard every access; a missing getX() result is a runtime Error, not a SoapFault.composer.json as --dev. A minor generator bump can change method signatures of the generated tree. Upgrade deliberately and diff.--struct-* options / a templates dir). If you need every struct to implement a common interface or extend a base class, do it here rather than post-processing files — post-processing is fragile.trace => 1 in the client options and inspect __getLastRequest() / __getLastResponse(). This is the fastest way to see what the vendor actually requires when the WSDL is misleading.How can I help you explore Laravel packages today?