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

0.1.0 2025-10-10 07:53 UTC

This package is auto-updated.

Last update: 2025-10-10 07:57:26 UTC


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.

PHP Version License Code Coverage

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
  • 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 acceptable
  • all(): Transactional operations requiring all-or-nothing
  • unwrap(): Simple scripts where async complexity isn't needed
  • each(): 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 a PromiseInterface 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 a value or reason

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:

  1. 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
  2. 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)
  3. 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:

  1. Sequential Dependencies: If Step 2 requires data from Step 1, you can't parallelize
  2. Server-Side Rate Limiting: If the API throttles concurrent requests, the server will queue them anyway
  3. 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:

Architecture

Core Components

  1. TransportInterface: Defines the contract for all transport implementations

    • doRequest(): Execute SOAP request
    • getLastRequest(): Retrieve last request body
    • getLastResponse(): Retrieve last response body
    • getLastRequestHeaders(): Get request headers
    • getLastResponseHeaders(): Get response headers
  2. SoapClient: Main SOAP client extending PHP's \SoapClient

    • Accepts custom transport via constructor
    • Delegates __doRequest() to transport layer
    • Supports trace mode for debugging
  3. Transport Implementations:

    • SoapClientTransport: Wraps standard PHP \SoapClient
    • HttpClientTransport: PSR-18 HTTP client adapter
    • GuzzleTransport: Guzzle-specific implementation with rich options
    • GuzzlePromiseTransport: Async promise-based transport
    • HttpMessageResponseTransport: 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:

  1. PSR Compliance: Adherence to PSR-7 and PSR-18 standards
  2. Type Safety: Strict typing throughout
  3. Transport Abstraction: Clear separation of concerns
  4. Backward Compatibility: Semantic versioning
  5. 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:

  1. Fixed: HttpClientTransport.php - ?callable type hints (PHP 8.2+ incompatible)
  2. Fixed: HttpMessageResponseSoapClient.php - Missing 'uri' and 'location' options
  3. Documented: GuzzleTransport.php line 95 - parse_url() misuse with bitwise flags
  4. Documented: HttpMessageTransport.php - Header formatting assumes simple arrays

See specs/001-unit-testing-implementation/BLOCKERS.md for details.

Contributing

Contributions are welcome! Please:

  1. Follow the project constitution principles
  2. Write tests for new features (aim for 80%+ coverage)
  3. Use strict typing (declare(strict_types=1))
  4. Follow PSR-12 coding standards
  5. 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