HTTP Clients And APIs

SOAP And XML APIs

Not every API is JSON over REST. Many finance, government, travel, logistics, healthcare, and legacy enterprise systems still use SOAP or XML APIs. A PHP developer does not need to love them, but they must be able to maintain integrations safely.

SOAP usually uses XML envelopes, WSDL contracts, named operations, strict schemas, and XML namespaces. Plain XML APIs may skip SOAP but still require careful parsing, validation, and error handling.

XML basics

XML is structured text with elements, attributes, and namespaces.

PHP example
<?php

declare(strict_types=1);

$xml = <<<'XML'
<order id="ord_123">
    <total currency="GBP">19.99</total>
</order>
XML;

$document = simplexml_load_string($xml);

if ($document === false) {
    throw new RuntimeException('Invalid XML.');
}

echo (string) $document['id'] . PHP_EOL;
echo (string) $document->total . ' ' . (string) $document->total['currency'] . PHP_EOL;

// Prints:
// ord_123
// 19.99 GBP

Do not parse XML with regular expressions. Use XML parsers such as SimpleXML, DOMDocument, XMLReader, or a SOAP client.

SOAP envelopes

SOAP wraps requests and responses in an envelope with a body. Real SOAP messages use namespaces heavily.

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetOrderResponse xmlns="https://api.example.test/orders">
      <OrderId>ord_123</OrderId>
    </GetOrderResponse>
  </soap:Body>
</soap:Envelope>

Namespaces are part of the element name. If code works only after removing namespaces from the XML string, it is usually hiding the real problem.

PHP example
<?php

declare(strict_types=1);

$xml = <<<'XML'
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:o="https://api.example.test/orders">
    <soap:Body>
        <o:GetOrderResponse>
            <o:OrderId>ord_123</o:OrderId>
        </o:GetOrderResponse>
    </soap:Body>
</soap:Envelope>
XML;

$document = simplexml_load_string($xml);

if ($document === false) {
    throw new RuntimeException('Invalid SOAP XML.');
}

$soap = $document->children('http://schemas.xmlsoap.org/soap/envelope/');
$body = $soap->Body;
$orders = $body->children('https://api.example.test/orders');

echo (string) $orders->GetOrderResponse->OrderId . PHP_EOL;

// Prints:
// ord_123

WSDL and SoapClient

A WSDL file describes SOAP operations, message types, endpoint URLs, and bindings. PHP has a built-in SoapClient extension that can read WSDL and call operations.

Conceptually, code may look like this:

PHP example
<?php

declare(strict_types=1);

$options = [
    'trace' => true,
    'exceptions' => true,
    'connection_timeout' => 10,
];

// $client = new SoapClient('https://api.example.test/service.wsdl', $options);
// $response = $client->__soapCall('GetOrder', [['OrderId' => 'ord_123']]);

print_r($options);

// Prints:
// [trace] => 1
// [exceptions] => 1
// [connection_timeout] => 10

The call is commented because it would require a real external service. In project code, check whether the soap PHP extension is installed and whether the WSDL is local, remote, cached, or generated.

Faults and errors

SOAP can return a SOAP Fault even when the HTTP status is not what a JSON API developer expects. Faults have structured fields such as code and message.

PHP example
<?php

declare(strict_types=1);

function soapFaultSummary(string $code, string $message): array
{
    return [
        'type' => 'soap_fault',
        'code' => $code,
        'message' => $message,
    ];
}

print_r(soapFaultSummary('Client.Authentication', 'Invalid credentials.'));

// Prints:
// [type] => soap_fault
// [code] => Client.Authentication
// [message] => Invalid credentials.

When debugging SOAP, preserve request IDs and safe excerpts of the request or response. Do not log credentials or full personal data.

XML security

XML parsers have security pitfalls. Be careful with external entities, very large documents, and untrusted input. Avoid enabling network access or external entity loading unless a trusted integration explicitly requires it.

For large XML files, prefer streaming parsers such as XMLReader instead of loading the whole document into memory.

What to check

Before moving on, make sure you can:

  • Explain why SOAP integrations often depend on WSDL contracts.
  • Read simple XML values and attributes.
  • Handle namespaces instead of stripping them.
  • Recognise where SoapClient fits in PHP.
  • Treat SOAP Faults as structured API errors.
  • Avoid unsafe XML parsing and excessive logging.

XML Names And Types Are Exact

SOAP messages are XML documents, so namespace URIs, qualified element names, order, and schema types matter. A prefix such as soap or ns1 is only a local alias; the namespace URI determines meaning. Comparing or constructing XML by string concatenation is fragile because equivalent documents can use different prefixes and whitespace.

Use DOM, XMLReader, XMLWriter, SimpleXML, or the SOAP extension according to the task. For large documents, streaming readers avoid loading the complete tree. Always configure parser behavior deliberately for untrusted or provider-controlled input.

SOAP Envelope And Operation

A SOAP envelope contains an optional header and a body. Headers can carry addressing, security, correlation, or provider-specific metadata. The body contains the operation request or response defined by the service contract. SOAP 1.1 and SOAP 1.2 use different namespaces, content types, and fault details; configure the version expected by the provider.

Do not infer success only from HTTP 200. Providers may use HTTP errors with SOAP Fault bodies, and some legacy systems return a SOAP Fault under HTTP 200. Parse both transport and envelope outcome and preserve a safe correlation identifier.

WSDL Is The Machine-Readable Contract

A WSDL describes services, ports, bindings, operations, messages, and XML Schema types. PHP's SoapClient can use it to serialize calls and deserialize responses. Generated PHP classes through a class map can be clearer than generic stdClass graphs, but they must match optional elements, arrays, namespaces, and date formats.

Cache production WSDLs according to deployment policy rather than fetching them unpredictably during every request. A provider changing a remote WSDL can alter behavior without an application release. Pin or vendor reviewed contracts when licensing and provider guidance permit, and monitor announced versions.

WSDLs are often supplemented by human rules about certificates, identifiers, operation order, and retries. Keep those rules with the integration; generated types alone do not define the workflow.

Configuring SoapClient

Set connection and overall timeouts, SOAP version, certificate options, proxy settings, compression, and trace behavior consciously. The PHP stream context controls TLS details for many integrations. Verify server names and certificate chains; disabling verification to make a test pass converts a connection problem into a security vulnerability.

Mutual TLS requires a client certificate and private key with a renewal process. Track expiration and test rotation before production cutover. Restrict filesystem permissions and avoid printing certificate paths or secrets in public errors.

The trace option can expose the last request and response, which is useful in a controlled diagnostic environment but risky in production because SOAP payloads often contain personal or regulated data. Sanitize captured XML and disable unrestricted debug output.

Mapping XML To Domain Values

Keep generated SOAP transport types at the adapter boundary. Convert them into application-owned commands and results with explicit validation. XML Schema decimal values should not be casually cast to float; preserve exact strings or map them through the application's decimal policy. Date-times need timezone and precision checks.

Optional, absent, empty, and xsi:nil elements can have different meanings. Test each form the provider documents. PHP object mapping can collapse distinctions if the class map or typemap is incomplete.

Large integer identifiers may exceed platform or JavaScript safe ranges. Keep them as strings when they are identifiers rather than quantities.

SOAP Faults And Business Rejections

A SOAP Fault has a code, reason, and optional detail. Provider detail often contains structured error identifiers. Translate known faults into application-owned outcomes while retaining sanitized provider codes for support.

Separate transport timeout, TLS failure, malformed XML, contract mismatch, authentication rejection, business rejection, and unknown server fault. Retry only outcomes the provider declares safe. A timeout after sending a state-changing request is ambiguous; query status or use the provider's idempotency/reference mechanism before repeating it.

Do not expose raw fault XML to users. It may contain internal hostnames, stack traces, identifiers, or personal data.

XML Parser Security

Untrusted XML can attempt external entity access, local file disclosure, network requests, or resource exhaustion. Use current PHP/libxml defaults and APIs, disable network or external resolution unless a trusted contract explicitly requires it, and do not enable legacy entity substitution globally.

Set request and response size limits before parsing. Deep nesting, huge text nodes, and compressed payloads can exhaust memory or CPU. Validate the expected root, namespace, and schema shape rather than accepting any well-formed XML.

XML signatures and encryption are specialized protocols with canonicalization and wrapping risks. Use a maintained implementation required by the provider and validate the signed element identity, not only that some signature verifies.

Contract Evolution

Providers may add optional elements, new enum values, or operations. A tolerant reader can ignore unknown optional fields while still rejecting a changed meaning for required data. Generated classes and exhaustive matches need tests against new values.

Store sanitized request and response fixtures for each supported contract version. When updating a WSDL, review generated-code diffs, namespace and endpoint changes, certificate requirements, and fault definitions. Run old and new clients against the provider sandbox before cutover.

Testing And Operations

Unit-test mapping with XML fixtures for normal responses, optional elements, nil values, decimals, dates, and every known fault. Integration-test TLS, certificates, SOAPAction or content-type requirements, namespaces, and timeouts against a sandbox or controlled stub.

Record latency, transport failures, fault categories, contract version, and correlation IDs. Avoid high-cardinality or sensitive XML in metrics. Alert before certificate expiration and on sustained changes in provider fault rates.

Exercise an ambiguous timeout for a state-changing operation and follow the documented reconciliation path. A successful local serialization test does not prove that the provider accepted, persisted, or deduplicated the request.

Interoperability Details Matter

Legacy SOAP integrations may use document/literal, RPC/encoded, multipart attachments, or provider-specific header conventions. Confirm the binding in the WSDL and test with the exact SOAP version. PHP's automatic mapping can differ from a Java or .NET client around single-item arrays, empty elements, and enum values.

When an array sometimes contains one item, ensure it is still decoded as a collection rather than one object. Class maps and typemaps can normalize these cases at the adapter boundary. Keep a fixture with zero, one, and several items because production defects often appear only at cardinality one.

For binary documents, MTOM or attachment mechanisms may be more appropriate than enormous base64 text inside the envelope, but support varies. Measure memory use and enforce size limits. A contract that permits large attachments needs streaming or staging behavior, malware scanning, retention policy, and an explicit failure response.

After this lesson, you should be able to reason about XML namespaces and SOAP envelopes, use WSDL and SoapClient deliberately, map transport types into domain values, classify faults and ambiguous outcomes, secure XML parsing and mutual TLS, and test contract evolution with representative fixtures.

Practice

Practice: Read A SOAP Response

Write a PHP function that extracts an order ID from a namespaced SOAP response.

Requirements

  • Use an XML parser rather than string matching.
  • Handle the SOAP namespace.
  • Handle the order namespace.
  • Return a clear result when XML is invalid.
  • Return a clear result when the order ID is missing.
  • Include examples for valid XML, invalid XML, and missing order ID.
Show solution

This solution navigates namespaces explicitly so the code matches the XML contract.

PHP example
<?php

declare(strict_types=1);

function orderIdFromSoapResponse(string $xml): array
{
    libxml_use_internal_errors(true);
    $document = simplexml_load_string($xml);
    libxml_clear_errors();

    if ($document === false) {
        return ['ok' => false, 'message' => 'SOAP response is not valid XML.'];
    }

    $soap = $document->children('http://schemas.xmlsoap.org/soap/envelope/');
    $body = $soap->Body ?? null;

    if ($body === null) {
        return ['ok' => false, 'message' => 'SOAP body is missing.'];
    }

    $orders = $body->children('https://api.example.test/orders');
    $orderId = trim((string) ($orders->GetOrderResponse->OrderId ?? ''));

    if ($orderId === '') {
        return ['ok' => false, 'message' => 'OrderId is missing.'];
    }

    return ['ok' => true, 'order_id' => $orderId];
}

$valid = <<<'XML'
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:o="https://api.example.test/orders">
    <soap:Body>
        <o:GetOrderResponse><o:OrderId>ord_123</o:OrderId></o:GetOrderResponse>
    </soap:Body>
</soap:Envelope>
XML;

$missingOrderId = <<<'XML'
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:o="https://api.example.test/orders">
    <soap:Body>
        <o:GetOrderResponse />
    </soap:Body>
</soap:Envelope>
XML;

$examples = [
    orderIdFromSoapResponse($valid),
    orderIdFromSoapResponse('<not closed'),
    orderIdFromSoapResponse($missingOrderId),
];

foreach ($examples as $result) {
    echo ($result['ok'] ? 'ok: ' . $result['order_id'] : 'error: ' . $result['message']) . PHP_EOL;
}

// Prints:
// ok: ord_123
// error: SOAP response is not valid XML.
// error: OrderId is missing.

The important part is that the code treats namespaces as part of the contract. Removing namespaces to make parsing easier usually makes the integration more fragile.