andrei-stepanov / soapclient-transports
Transports for PHP SoapClient
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/andrei-stepanov/soapclient-transports
Requires
- php: ^7.2||^8.0
- ext-soap: ^7||^8
- psr/http-client: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
Suggests
- guzzlehttp/guzzle: For HTTP client implementation
README
A modern, PSR-compliant PHP library for flexible SOAP client transport implementations with support for PSR-7/PSR-18 standards, Guzzle HTTP client, and asynchronous promise-based requests.
Features
- ✅ PSR Compliance: Full support for PSR-7 (HTTP Message), PSR-18 (HTTP Client)
- ✅ Type Safety: Strict typing with
declare(strict_types=1)
throughout - ✅ Transport Abstraction: Pluggable transport layer via
TransportInterface
- ✅ Multiple Implementations:
- Standard PHP
\SoapClient
wrapper - PSR-7/PSR-18 HTTP message transport
- Guzzle HTTP client support
- Asynchronous promise-based transport
- Standard PHP
- ✅ Backward Compatible: Semant ically versioned for safe upgrades
- ✅ Well Tested: 111 tests, 192 assertions, 85%+ code coverage
Requirements
- PHP 8.1 or higher
ext-soap
PHP extension- PSR-7 HTTP Message implementation (e.g.,
guzzlehttp/psr7
) - PSR-18 HTTP Client implementation (e.g.,
guzzlehttp/guzzle
)
Installation
composer require andrei-stepanov/soap-client-transports
Quick Start
Basic Usage with PSR-18 Client
<?php use AndreiStepanov\SoapClientTransports\SoapClient; use AndreiStepanov\SoapClientTransports\Requests\HttpClientTransport; use GuzzleHttp\Client; // Create a PSR-18 HTTP client $httpClient = new Client(); // Create a transport $transport = new HttpClientTransport($httpClient); // Create SOAP client with custom transport $client = new SoapClient( wsdl: 'http://example.com/service?wsdl', options: ['trace' => true], transport: $transport ); // Make SOAP calls $result = $client->someMethod(['param' => 'value']);
Guzzle Transport with Options
<?php use AndreiStepanov\SoapClientTransports\Requests\GuzzleTransport; use AndreiStepanov\SoapClientTransports\SoapClient; use GuzzleHttp\Client; $guzzleClient = new Client(); $options = [ 'authentication' => SOAP_AUTHENTICATION_BASIC, 'username' => 'user', 'password' => 'pass', 'timeout' => 30, 'user_agent' => 'MyApp/1.0', 'compression' => SOAP_COMPRESSION_GZIP, ]; $transport = new GuzzleTransport($guzzleClient, $options); $client = new SoapClient('http://example.com/service?wsdl', $options, $transport); $result = $client->someMethod(['data' => 'value']);
Asynchronous Promises with Guzzle
<?php use AndreiStepanov\SoapClientTransports\GuzzlePromiseSoapClient; use GuzzleHttp\Client; $guzzleClient = new Client(); $client = new GuzzlePromiseSoapClient( wsdl: 'http://example.com/service?wsdl', options: ['trace' => true], client: $guzzleClient ); // Returns a GuzzleHttp\Promise\PromiseInterface $promise = $client->__soapCall('someMethod', [['param' => 'value']]); // Wait for the promise to resolve $result = $promise->wait();
Promise Utilities Reference
When working with multiple concurrent promises, Guzzle provides utilities to orchestrate their resolution:
Utility | Purpose | Returns | Error Behavior |
---|---|---|---|
Utils::settle() |
Wait for all promises (success + failure) | Promise → Settlement array | Never rejects |
Utils::all() |
Wait for all to succeed | Promise → Result array | Rejects on first failure |
Utils::unwrap() |
Synchronous wait for all | Result array directly | Throws on first failure |
Utils::each() |
Process promises as they complete | Promise → null | Depends on callbacks |
When to use each:
settle()
: Batch operations where partial failures are acceptableall()
: Transactional operations requiring all-or-nothingunwrap()
: Simple scripts where async complexity isn't neededeach()
: Long-running jobs with progress reporting
For detailed examples of concurrent request patterns, see the Concurrent SOAP Requests section below.
Response-Only Transport (Testing)
<?php use AndreiStepanov\SoapClientTransports\HttpMessageResponseSoapClient; use GuzzleHttp\Psr7\Response; // Create a mock response for testing $response = new Response(200, [], '<soap:Envelope>...</soap:Envelope>'); $client = new HttpMessageResponseSoapClient(null, $response); // Returns the pre-configured response $result = $client();
Concurrent SOAP Requests
Concurrent requests allow you to execute multiple SOAP calls simultaneously, dramatically reducing total response time when calling multiple services or performing batch operations. Unlike sequential requests (where each call waits for the previous to complete), concurrent requests are initiated together and resolve in parallel.
This section shows you how to leverage Guzzle's promise utilities to build high-performance SOAP integrations. Whether you're aggregating data from multiple services, performing batch operations, or optimizing high-latency network calls, concurrent requests can reduce total response time by 70-90%.
Prerequisites: Familiarity with promises and the GuzzlePromiseSoapClient
class.
What You'll Learn:
- Execute multiple SOAP requests concurrently
- Handle mixed success/failure scenarios gracefully
- Choose the right promise utility for your use case
- Optimize performance and avoid common pitfalls
Basic Concurrent Pattern
The foundational pattern for concurrent SOAP requests uses Utils::settle()
to ensure all promises complete, regardless of success or failure:
<?php declare(strict_types=1); use AndreiStepanov\SoapClientTransports\GuzzlePromiseSoapClient; use GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; $client = new Client(); // Create multiple SOAP client instances $soap1 = new GuzzlePromiseSoapClient('https://service.example.com/api?wsdl', [], $client); $soap2 = new GuzzlePromiseSoapClient('https://service.example.com/api?wsdl', [], $client); $soap3 = new GuzzlePromiseSoapClient('https://service.example.com/api?wsdl', [], $client); // Initiate all requests concurrently (non-blocking) $promises = [ 'users' => $soap1->__soapCall('GetUsers', [['limit' => 100]]), 'products' => $soap2->__soapCall('GetProducts', [['category' => 'electronics']]), 'orders' => $soap3->__soapCall('GetOrders', [['status' => 'pending']]), ]; // Wait for all promises to complete (success or failure) $results = Utils::settle($promises)->wait(); // Process results - check state for each foreach ($results as $key => $result) { if ($result['state'] === 'fulfilled') { // Success - process the SOAP response echo "✅ {$key}: " . print_r($result['value'], true) . "\n"; } else { // Failure - handle the exception echo "❌ {$key} failed: " . $result['reason']->getMessage() . "\n"; } }
Key Points:
- Each
__soapCall()
returns aPromiseInterface
immediately (non-blocking) - Promises are collected in an associative array for easy identification
Utils::settle()
waits for all promises regardless of success/failure- Each result has a
state
('fulfilled' or 'rejected') and either avalue
orreason
See Also: Full working example in tests/test.php
When to Use Concurrent Requests
Concurrent SOAP requests provide significant performance benefits in specific scenarios:
✅ When Concurrent Requests Help:
-
Multiple Independent Services
- Scenario: Calling 4 different SOAP endpoints
- Sequential time: 4 × 500ms = 2000ms
- Concurrent time: ~500-600ms (max latency)
- Benefit: 70-75% time reduction
-
Batch Operations to Same Service
- Scenario: 10 independent operations (e.g., fetching 10 user profiles)
- Sequential time: 10 × 200ms = 2000ms
- Concurrent time: 200-300ms
- Benefit: 85-90% time reduction (if server supports parallel processing)
-
High-Latency Networks
- Scenario: 5 calls over WAN connection (200ms RTT each)
- Sequential latency overhead: 5 × 200ms = 1000ms
- Concurrent latency overhead: ~200ms
- Benefit: Minimizes round-trip time impact
❌ When Concurrent Requests Don't Help:
- Sequential Dependencies: If Step 2 requires data from Step 1, you can't parallelize
- Server-Side Rate Limiting: If the API throttles concurrent requests, the server will queue them anyway
- Very Fast Requests: For 2-3 calls @ 50ms each, the promise overhead may exceed the benefit
Resource Considerations:
- Memory: Each pending promise holds request/response data in memory
- Connections: Respect server connection limits and capacity
- Server Load: Don't overwhelm SOAP services with excessive concurrency
Error Handling Strategies
When working with concurrent requests, not all promises will succeed. Different scenarios require different error handling approaches. Choose the strategy that matches your reliability requirements and business logic.
Strategy 1: Fail-Fast (Transactional)
Use Utils::all()
when all requests must succeed for the operation to be valid (e.g., multi-step workflows, atomic operations):
<?php declare(strict_types=1); use GuzzleHttp\Promise\Utils; use GuzzleHttp\Promise\RejectionException; try { // All promises must succeed $results = Utils::all($promises)->wait(); // All succeeded - process results foreach ($results as $key => $value) { echo "✅ {$key}: Success\n"; } } catch (RejectionException $e) { // At least one failed - entire operation fails echo "❌ Operation failed: " . $e->getReason()->getMessage() . "\n"; // Rollback or compensate as needed }
When to use: Transactional operations, workflows where partial success is meaningless.
Strategy 2: Settle (Partial Failures OK)
Use Utils::settle()
when you need results from all requests, even if some fail (e.g., data aggregation, batch processing):
<?php declare(strict_types=1); use GuzzleHttp\Promise\Utils; // Wait for all to complete (never rejects) $results = Utils::settle($promises)->wait(); $successes = []; $failures = []; foreach ($results as $key => $result) { if ($result['state'] === 'fulfilled') { $successes[$key] = $result['value']; } else { $failures[$key] = $result['reason']->getMessage(); } } // Process what succeeded echo "✅ Successful: " . count($successes) . " / " . count($results) . "\n"; foreach ($successes as $key => $value) { // Process successful results } // Log or handle failures if (!empty($failures)) { echo "❌ Failed: " . count($failures) . "\n"; foreach ($failures as $key => $error) { error_log("Request '{$key}' failed: {$error}"); } }
When to use: Batch operations, data aggregation where partial results are useful.
Strategy 3: Individual Handling (Maximum Resilience)
Process each result with individual error handling for maximum isolation:
<?php declare(strict_types=1); use GuzzleHttp\Promise\Utils; $results = Utils::settle($promises)->wait(); foreach ($results as $key => $result) { try { if ($result['state'] === 'fulfilled') { $data = $result['value']; // Process successfully processData($key, $data); } else { // Log failure but continue error_log("Skipping '{$key}': " . $result['reason']->getMessage()); } } catch (\Exception $e) { // Catch processing errors too error_log("Error processing '{$key}': " . $e->getMessage()); continue; // Keep going } }
When to use: Best-effort processing, when you want to isolate failures and continue processing remaining items.
Advanced Patterns
For more sophisticated use cases, Guzzle provides additional promise utilities beyond the basic patterns.
Synchronous-Style with unwrap()
When you need concurrent execution but prefer simple synchronous-style code (useful for scripts and batch jobs):
<?php declare(strict_types=1); use GuzzleHttp\Promise\Utils; try { // Blocks until all resolve, returns plain array $results = Utils::unwrap($promises); // Direct array access (no promise objects) foreach ($results as $key => $value) { echo "{$key}: " . print_r($value, true) . "\n"; } } catch (\Exception $e) { // Any failure throws exception echo "Error: " . $e->getMessage() . "\n"; }
When to use: Simple scripts where you don't need fine-grained error handling. Note: This is a convenience wrapper around Utils::all()->wait()
.
Streaming with each()
Process promises as they complete (in completion order, not array order) - ideal for progress reporting:
<?php declare(strict_types=1); use GuzzleHttp\Promise\Utils; $completed = 0; $total = count($promises); Utils::each( $promises, // onFulfilled: called for each success function ($value, $key) use (&$completed, $total) { $completed++; echo "[{$completed}/{$total}] ✅ {$key} completed\n"; // Process result immediately processResult($key, $value); }, // onRejected: called for each failure function ($reason, $key) use (&$completed, $total) { $completed++; echo "[{$completed}/{$total}] ❌ {$key} failed: {$reason->getMessage()}\n"; } )->wait(); echo "All {$total} requests completed\n";
When to use: Long-running batch jobs where you want to show progress or process results as soon as they're available. Results are processed in completion order, not the order they appear in the array.
Complete Example: Multi-Service Data Aggregation
Here's a production-ready example combining multiple patterns - fetching data from three independent SOAP services with proper error handling:
<?php declare(strict_types=1); use AndreiStepanov\SoapClientTransports\GuzzlePromiseSoapClient; use GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; // Initialize HTTP client $httpClient = new Client(['timeout' => 30]); // Create SOAP clients for different services $userService = new GuzzlePromiseSoapClient('https://users-api.example.com/soap?wsdl', [], $httpClient); $orderService = new GuzzlePromiseSoapClient('https://orders-api.example.com/soap?wsdl', [], $httpClient); $inventoryService = new GuzzlePromiseSoapClient('https://inventory-api.example.com/soap?wsdl', [], $httpClient); // Initiate concurrent requests $promises = [ 'user' => $userService->__soapCall('GetUserProfile', [['userId' => 12345]]), 'orders' => $orderService->__soapCall('GetUserOrders', [['userId' => 12345, 'limit' => 10]]), 'inventory' => $inventoryService->__soapCall('CheckStock', [['productIds' => [101, 102, 103]]]), ]; // Wait for all to complete $results = Utils::settle($promises)->wait(); // Aggregate results with error handling $aggregated = [ 'userId' => 12345, 'timestamp' => date('Y-m-d H:i:s'), 'data' => [], 'errors' => [], ]; foreach ($results as $service => $result) { if ($result['state'] === 'fulfilled') { $aggregated['data'][$service] = $result['value']; } else { $aggregated['errors'][$service] = $result['reason']->getMessage(); error_log("Service '{$service}' failed: " . $result['reason']->getMessage()); } } // Decide based on results if (count($aggregated['errors']) === count($promises)) { throw new \RuntimeException('All services failed'); } // Process with partial data echo "✅ Successfully retrieved " . count($aggregated['data']) . " / " . count($promises) . " services\n"; return $aggregated;
Next Steps:
- See full working example in
tests/test.php
- Learn more about Guzzle Promises
- Review the Project Constitution for development principles
Architecture
Core Components
-
TransportInterface
: Defines the contract for all transport implementationsdoRequest()
: Execute SOAP requestgetLastRequest()
: Retrieve last request bodygetLastResponse()
: Retrieve last response bodygetLastRequestHeaders()
: Get request headersgetLastResponseHeaders()
: Get response headers
-
SoapClient
: Main SOAP client extending PHP's\SoapClient
- Accepts custom transport via constructor
- Delegates
__doRequest()
to transport layer - Supports trace mode for debugging
-
Transport Implementations:
SoapClientTransport
: Wraps standard PHP\SoapClient
HttpClientTransport
: PSR-18 HTTP client adapterGuzzleTransport
: Guzzle-specific implementation with rich optionsGuzzlePromiseTransport
: Async promise-based transportHttpMessageResponseTransport
: Response-only (testing)GuzzlePromiseResponseTransport
: Promise-based response wrapper
Supported SOAP Options
The library supports standard PHP SOAP options:
- Authentication:
SOAP_AUTHENTICATION_BASIC
,SOAP_AUTHENTICATION_DIGEST
- SSL/TLS:
local_cert
,passphrase
- Proxy:
proxy_host
,proxy_port
,proxy_login
,proxy_password
- Compression:
SOAP_COMPRESSION_GZIP
,SOAP_COMPRESSION_DEFLATE
- Timeouts:
timeout
,connection_timeout
- Headers:
user_agent
,keep_alive
- Debugging:
trace
Testing
Run Unit Tests
# All tests vendor/bin/codecept run Unit # With coverage vendor/bin/codecept run Unit --coverage --coverage-html # Specific test vendor/bin/codecept run Unit/GuzzleTransportTest
Coverage Reports
- HTML Report:
coverage/index.html
- XML Report:
coverage.xml
- Current Coverage: 85.34% lines, 79.41% methods
Development
Project Structure
workspace/
├── src/
│ ├── TransportInterface.php
│ ├── SoapClient.php
│ ├── SoapClientTransport.php
│ ├── HttpMessageTransport.php
│ ├── HttpMessageResponseSoapClient.php
│ ├── GuzzlePromiseSoapClient.php
│ ├── GuzzleSoapClient.php
│ ├── Requests/
│ │ ├── HttpClientTransport.php
│ │ ├── GuzzleTransport.php
│ │ └── GuzzlePromiseTransport.php
│ └── Responses/
│ ├── HttpMessageResponseTransport.php
│ └── GuzzlePromiseResponseTransport.php
├── tests/
│ ├── Unit/
│ └── _data/
└── specs/
└── 001-unit-testing-implementation/
Code Quality
- Type Safety: All files use
declare(strict_types=1)
- PSR Standards: PSR-7, PSR-18 compliance
- Documentation: Comprehensive PHPDoc blocks
- Testing: 111 unit tests with 85%+ coverage
Constitution & Principles
This project follows strict development principles:
- PSR Compliance: Adherence to PSR-7 and PSR-18 standards
- Type Safety: Strict typing throughout
- Transport Abstraction: Clear separation of concerns
- Backward Compatibility: Semantic versioning
- Testing Discipline: Minimum 80% code coverage
See .specify/memory/constitution.md
for details.
Known Limitations
WSDL-Dependent Features
Some features require actual WSDL files for full testing:
GuzzlePromiseSoapClient::__soapCall()
- Requires real SOAP operations- Complex type marshalling - WSDL-specific behavior
Production Code Issues Discovered
During testing, several production code issues were identified:
- Fixed:
HttpClientTransport.php
-?callable
type hints (PHP 8.2+ incompatible) - Fixed:
HttpMessageResponseSoapClient.php
- Missing 'uri' and 'location' options - Documented:
GuzzleTransport.php
line 95 -parse_url()
misuse with bitwise flags - Documented:
HttpMessageTransport.php
- Header formatting assumes simple arrays
See specs/001-unit-testing-implementation/BLOCKERS.md
for details.
Contributing
Contributions are welcome! Please:
- Follow the project constitution principles
- Write tests for new features (aim for 80%+ coverage)
- Use strict typing (
declare(strict_types=1)
) - Follow PSR-12 coding standards
- Update documentation as needed
License
This project is licensed under the MIT License - see the LICENSE file for details.
Credits
- Original concept: Andrei Stepanov
- Testing implementation: Comprehensive Codeception/PHPUnit test suite
- CI/CD: GitHub Actions workflow
Changelog
See CHANGELOG.md for version history and release notes.
Support
- Issues: GitHub Issues
- Documentation:
specs/001-unit-testing-implementation/
- Coverage Report:
coverage/index.html
(after running tests with--coverage-html
)