Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce message encoders for building soap servers #32

Merged
merged 1 commit into from
Jan 3, 2025

Conversation

veewee
Copy link
Member

@veewee veewee commented Jan 2, 2025

Q A
Type feature
BC Break no
Fixed issues

Summary

This PR makes encoding / decoding methods isomorphic so that the logic can be used for both SOAP servers and clients.

Building a SOAP server:

<?php declare(strict_types=1);

require_once \dirname(__DIR__) . '/vendor/autoload.php';

use Soap\Encoding\Encoder\Method\MethodContext;
use Soap\Encoding\Encoder\Method\RequestEncoder;
use Soap\Encoding\Encoder\Method\ResponseEncoder;
use Soap\Encoding\EncoderRegistry;
use Soap\Wsdl\Loader\StreamWrapperLoader;
use Soap\WsdlReader\Locator\ServiceSelectionCriteria;
use Soap\WsdlReader\Metadata\Wsdl1MetadataProvider;
use Soap\WsdlReader\Wsdl1Reader;

$wsdlLocation = __DIR__ . '/calc.wsdl';
$wsdl = (new Wsdl1Reader(new StreamWrapperLoader()))($wsdlLocation);
$registry ??= EncoderRegistry::default()
    ->addClassMap('http://tempuri.org/', 'Add', Add::class)
    ->addClassMap('http://tempuri.org/', 'AddResponse', AddResponse::class);
$metadataProvider = new Wsdl1MetadataProvider($wsdl, ServiceSelectionCriteria::defaults());
$metadata = $metadataProvider->getMetadata();

// The soap action can be detected from a PSR-7 request headers by using:
// https://github.com/php-soap/psr18-transport/blob/main/src/HttpBinding/SoapActionDetector.php
$soapAction = 'http://tempuri.org/Add';

$methodContext = new MethodContext(
    $method = $metadata->getMethods()->fetchBySoapAction($soapAction),
    $metadata,
    $registry,
    $wsdl->namespaces,
);

$request = <<<EOXML
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <soap:Body>
            <Add xmlns="http://tempuri.org/">
                <a>1</a>
                <b>2</b>
            </Add>
        </soap:Body>
    </soap:Envelope>
EOXML;

$requestEncoder = new RequestEncoder();
$requestIso = $requestEncoder->iso($methodContext);
$arguments = $requestIso->from($request);

var_dump($arguments);

final class Add
{
    public int $a;
    public int $b;
}
final class AddResponse
{
    public function __construct(
        public int $AddResult,
    ) {
    }
}

$myCalculator = new class() {
    public function Add(Add $add): AddResponse
    {
        return new AddResponse($add->a + $add->b);
    }
};


$result = $myCalculator->{$method->getName()}(...$arguments);

var_dump($result);


$responseEncoder = new ResponseEncoder();
$responseIso = $responseEncoder->iso($methodContext);
$response = $responseIso->to([$result]);

var_dump($response);
examples/calc-http-server.php:46:
array(1) {
  [0] =>
  class Add#1877 (2) {
    public int $a =>
    int(1)
    public int $b =>
    int(2)
  }
}
examples/calc-http-server.php:71:
class AddResponse#1942 (1) {
  public int $AddResult =>
  int(3)
}
examples/calc-http-server.php:78:
string(320) "<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><tns:AddResponse xmlns:tns="http://tempuri.org/"><tns:AddResult xmlns:tns="http://tempuri.org/">3</tns:AddResult></tns:AddResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>
"

/cc @rauanmayemir

@veewee veewee marked this pull request as draft January 2, 2025 15:33
@veewee veewee force-pushed the message-encoders branch 4 times, most recently from 8381be4 to a593b6a Compare January 3, 2025 08:36
@veewee veewee marked this pull request as ready for review January 3, 2025 08:36
@veewee veewee added the enhancement New feature or request label Jan 3, 2025
@veewee veewee force-pushed the message-encoders branch 2 times, most recently from 6c59395 to 82844b9 Compare January 3, 2025 08:59
@veewee
Copy link
Member Author

veewee commented Jan 3, 2025

@rauanmayemir This PR should provide the tools you need in order to build a SOAP server.
I've added a little example on how it can be used to build a dummy calculator webservice.

Can you test it out on your project as well?
Are the newly provided method encoders sufficient for your use-case or are you still missing some things?

Thanks!

@rauanmayemir
Copy link
Collaborator

Going to check it ASAP. 🔥

@rauanmayemir
Copy link
Collaborator

One unexpected issue I have currently is that $this->metadata->getMethods()->fetchBySoapAction($soapAction) expects $soapAction to be a fully-qualified 'uri' like http://soap.service.location#methodName.

I used to do something like:

$soapAction = \Psl\Str\trim($request->getHeaderLine('SOAPAction'), ' \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"');
// $methodName = last(explode('#', $soapAction, 2));

and then look up methods by name, but now I need to make sure FQDNs are present and valid.

I wonder if it could be made a bit smarter to account for requests made with headers like SOAPAction: methodName, e.g have a way to add a custom resolver that will account for non-compliant servers/clients. (it could be filed as a 'nice to have' feature request)

@rauanmayemir
Copy link
Collaborator

Other than that, happy to report this seems to be working.

@veewee
Copy link
Member Author

veewee commented Jan 3, 2025

I'm a bit confused about your comment:

One unexpected issue I have currently is that $this->metadata->getMethods()->fetchBySoapAction($soapAction) expects $soapAction to be a fully-qualified 'uri' like http://soap.service.location#methodName.

There is no check about fully-qualifiedness of the uri inside this codebase. It's all just strings with string comparisons.
The action contains whatever is inside the binding.operation.soap:operation soapAction attribute.

Running fetchBySoapAction searches for the method that matches this soapAction attribute to the requested SOAPAction.

Could you give me a more detailed example of what is going wrong exactly in your case?

I wonder if it could be made a bit smarter to account for requests made with headers like SOAPAction: methodName,

I don't think this is compliant. It should match the configured WSDL soapAction. Not the binding.operation name.

e.g have a way to add a custom resolver that will account for non-compliant servers/clients. (it could be filed as a 'nice to have' feature request)

You have access to the method collection, so you could iterate over them and match based on whatever you like to match it on. I've added the fetchBySoapAction to make it easier to directly fetch based on an action, but it doesn't mean you have to use it if your service somehow accepts unkown soap actions.

@rauanmayemir
Copy link
Collaborator

Nevermind, there's a companion method fetchByName which precisely solves my issue.

Could you give me a more detailed example of what is going wrong exactly in your case?

Sometimes I have no control over what's being sent in SOAPAction header, because the client pretty much omits the header. (this is usually an obsolete messy wsdl schema I'm supposed to implement in order to receive 'webhooks' from the upstream)

@veewee
Copy link
Member Author

veewee commented Jan 3, 2025

@rauanmayemir Allright. Thanks for testing it out!

@veewee veewee merged commit 0c16bc7 into php-soap:main Jan 3, 2025
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants