From e7745a6222ee92a565fd1e6fed2995b5bf5101e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 11 Sep 2018 21:58:39 +0200 Subject: [PATCH 1/3] Add support for optional SOAP options --- README.md | 29 +++++++++++++++++++++++++++-- src/Client.php | 35 +++++++++++++++++++++++++++++++---- tests/FunctionalTest.php | 20 +++++++++++++++++--- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f68db86..4710fc9 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,17 @@ WebService server. It requires a [`Browser`](https://github.com/clue/reactphp-buzz#browser) object bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) -in order to handle async requests and the WSDL file contents: +in order to handle async requests, the WSDL file contents and an optional +array of SOAP options: ```php $loop = React\EventLoop\Factory::create(); $browser = new Clue\React\Buzz\Browser($loop); -$client = new Client($browser, $wsdl); +$wsdl = ' 'http://example.com' +)); +``` + +If you find an option is missing or not supported here, PRs are much +appreciated! + If you want to call RPC functions, see below for the [`Proxy`](#proxy) class. Note: It's recommended (and easier) to wrap the `Client` in a [`Proxy`](#proxy) instance. @@ -207,6 +229,9 @@ same location and accessing the first location is sufficient. assert('http://example.com/soap/service' === $client->getLocation(0)); ``` +When the `location` option has been set in the `Client` constructor, this +method returns the value of the given `location` option. + Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. ### Proxy diff --git a/src/Client.php b/src/Client.php index 3a11a16..520dfe3 100644 --- a/src/Client.php +++ b/src/Client.php @@ -15,13 +15,17 @@ * * It requires a [`Browser`](https://github.com/clue/reactphp-buzz#browser) object * bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) - * in order to handle async requests and the WSDL file contents: + * in order to handle async requests, the WSDL file contents and an optional + * array of SOAP options: * * ```php * $loop = React\EventLoop\Factory::create(); * $browser = new Clue\React\Buzz\Browser($loop); * - * $client = new Client($browser, $wsdl); + * $wsdl = ' 'http://example.com' + * )); + * ``` + * + * If you find an option is missing or not supported here, PRs are much + * appreciated! + * * If you want to call RPC functions, see below for the [`Proxy`](#proxy) class. * * Note: It's recommended (and easier) to wrap the `Client` in a [`Proxy`](#proxy) instance. @@ -95,12 +117,14 @@ final class Client * * @param Browser $browser * @param string $wsdlContents + * @param array $options */ - public function __construct(Browser $browser, $wsdlContents) + public function __construct(Browser $browser, $wsdlContents, array $options = array()) { $this->browser = $browser; $this->encoder = new ClientEncoder( - 'data://text/plain;base64,' . base64_encode($wsdlContents) + 'data://text/plain;base64,' . base64_encode($wsdlContents), + $options ); $this->decoder = new ClientDecoder(); } @@ -191,6 +215,9 @@ public function getTypes() * assert('http://example.com/soap/service' === $client->getLocation(0)); * ``` * + * When the `location` option has been set in the `Client` constructor, this + * method returns the value of the given `location` option. + * * Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. * * @param string|int $function diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 2abcd88..430095e 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -21,12 +21,17 @@ class FunctionalTest extends TestCase */ private $client; - public function setUp() + // download WSDL file only once for all test cases + private static $wsdl; + public static function setUpBeforeClass() { - $wsdl = file_get_contents('http://www.thomas-bayer.com/axis2/services/BLZService?wsdl'); + self::$wsdl = file_get_contents('http://www.thomas-bayer.com/axis2/services/BLZService?wsdl'); + } + public function setUp() + { $this->loop = React\EventLoop\Factory::create(); - $this->client = new Client(new Browser($this->loop), $wsdl); + $this->client = new Client(new Browser($this->loop), self::$wsdl); } public function testBlzService() @@ -106,4 +111,13 @@ public function testGetLocationForUnknownFunctionNumberFails() { $this->assertEquals('http://www.thomas-bayer.com/axis2/services/BLZService', $this->client->getLocation(100)); } + + public function testGetLocationWithExplicitLocationOptionReturnsAsIs() + { + $this->client = new Client(new Browser($this->loop), self::$wsdl, array( + 'location' => 'http://example.com/' + )); + + $this->assertEquals('http://example.com/', $this->client->getLocation(0)); + } } From 80767bde56c771f1dd876d4fa0b6f1ca43aeb233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 11 Sep 2018 22:31:18 +0200 Subject: [PATCH 2/3] Support non-WSDL mode --- README.md | 36 ++++++++++----- ...{client-blz.php => 01-client-blz-wsdl.php} | 0 examples/02-client-blz-non-wsdl.php | 31 +++++++++++++ .../{client-wsdl.php => 11-wsdl-info.php} | 0 src/Client.php | 44 +++++++++++++------ tests/ClientTest.php | 35 +++++++++++++++ tests/FunctionalTest.php | 19 ++++++++ tests/Protocol/ClientEncoderTest.php | 21 +++++++++ 8 files changed, 162 insertions(+), 24 deletions(-) rename examples/{client-blz.php => 01-client-blz-wsdl.php} (100%) create mode 100644 examples/02-client-blz-non-wsdl.php rename examples/{client-wsdl.php => 11-wsdl-info.php} (100%) diff --git a/README.md b/README.md index 4710fc9..d3f2f33 100644 --- a/README.md +++ b/README.md @@ -146,14 +146,27 @@ try { error instead of throwing a `SoapFault`. It is not recommended to use this extension in production, so this should only ever affect test environments. -The `Client` constructor accepts an optional array of options. All given -options will be passed through to the underlying `SoapClient`. However, not -all options make sense in this async implementation and as such may not have -the desired effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php) +The `Client` constructor accepts an array of options. All given options will +be passed through to the underlying `SoapClient`. However, not all options +make sense in this async implementation and as such may not have the desired +effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php) documentation for more details. -The `location` option can be used to explicitly overwrite the URL of the SOAP -server to send the request to: +If working in WSDL mode, the `$options` parameter is optional. If working in +non-WSDL mode, the WSDL parameter must be set to `null` and the options +parameter must contain the `location` and `uri` options, where `location` is +the URL of the SOAP server to send the request to, and `uri` is the target +namespace of the SOAP service: + +```php +$client = new Client($browser, null, array( + 'location' => 'http://example.com', + 'uri' => 'http://ping.example.com', +)); +``` + +Similarly, if working in WSDL mode, the `location` option can be used to +explicitly overwrite the URL of the SOAP server to send the request to: ```php $client = new Client($browser, $wsdl, array( @@ -188,19 +201,21 @@ $promise = $proxy->ping('hello', 42); #### getFunctions() -The `getFunctions(): string[]` method can be used to +The `getFunctions(): string[]|null` method can be used to return an array of functions defined in the WSDL. It returns the equivalent of PHP's [`SoapClient::__getFunctions()`](http://php.net/manual/en/soapclient.getfunctions.php). +In non-WSDL mode, this method returns `null`. #### getTypes() -The `getTypes(): string[]` method can be used to +The `getTypes(): string[]|null` method can be used to return an array of types defined in the WSDL. It returns the equivalent of PHP's [`SoapClient::__getTypes()`](http://php.net/manual/en/soapclient.gettypes.php). +In non-WSDL mode, this method returns `null`. #### getLocation() @@ -229,8 +244,9 @@ same location and accessing the first location is sufficient. assert('http://example.com/soap/service' === $client->getLocation(0)); ``` -When the `location` option has been set in the `Client` constructor, this -method returns the value of the given `location` option. +When the `location` option has been set in the `Client` constructor +(such as when in non-WSDL mode), this method returns the value of the +given `location` option. Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. diff --git a/examples/client-blz.php b/examples/01-client-blz-wsdl.php similarity index 100% rename from examples/client-blz.php rename to examples/01-client-blz-wsdl.php diff --git a/examples/02-client-blz-non-wsdl.php b/examples/02-client-blz-non-wsdl.php new file mode 100644 index 0000000..2618f53 --- /dev/null +++ b/examples/02-client-blz-non-wsdl.php @@ -0,0 +1,31 @@ + 'http://www.thomas-bayer.com/axis2/services/BLZService', + 'uri' => 'http://thomas-bayer.com/blz/', + 'use' => SOAP_LITERAL +)); +$api = new Proxy($client); + +$api->getBank(new SoapVar($blz, XSD_STRING, null, null, 'blz', 'http://thomas-bayer.com/blz/'))->then( + function ($result) { + echo 'SUCCESS!' . PHP_EOL; + var_dump($result); + }, + function (Exception $e) { + echo 'ERROR: ' . $e->getMessage() . PHP_EOL; + } +); + +$loop->run(); diff --git a/examples/client-wsdl.php b/examples/11-wsdl-info.php similarity index 100% rename from examples/client-wsdl.php rename to examples/11-wsdl-info.php diff --git a/src/Client.php b/src/Client.php index 520dfe3..846ab24 100644 --- a/src/Client.php +++ b/src/Client.php @@ -83,14 +83,27 @@ * error instead of throwing a `SoapFault`. It is not recommended to use this * extension in production, so this should only ever affect test environments. * - * The `Client` constructor accepts an optional array of options. All given - * options will be passed through to the underlying `SoapClient`. However, not - * all options make sense in this async implementation and as such may not have - * the desired effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php) + * The `Client` constructor accepts an array of options. All given options will + * be passed through to the underlying `SoapClient`. However, not all options + * make sense in this async implementation and as such may not have the desired + * effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php) * documentation for more details. * - * The `location` option can be used to explicitly overwrite the URL of the SOAP - * server to send the request to: + * If working in WSDL mode, the `$options` parameter is optional. If working in + * non-WSDL mode, the WSDL parameter must be set to `null` and the options + * parameter must contain the `location` and `uri` options, where `location` is + * the URL of the SOAP server to send the request to, and `uri` is the target + * namespace of the SOAP service: + * + * ```php + * $client = new Client($browser, null, array( + * 'location' => 'http://example.com', + * 'uri' => 'http://ping.example.com', + * )); + * ``` + * + * Similarly, if working in WSDL mode, the `location` option can be used to + * explicitly overwrite the URL of the SOAP server to send the request to: * * ```php * $client = new Client($browser, $wsdl, array( @@ -115,15 +128,15 @@ final class Client /** * Instantiate a new SOAP client for the given WSDL contents. * - * @param Browser $browser - * @param string $wsdlContents - * @param array $options + * @param Browser $browser + * @param string|null $wsdlContents + * @param array $options */ public function __construct(Browser $browser, $wsdlContents, array $options = array()) { $this->browser = $browser; $this->encoder = new ClientEncoder( - 'data://text/plain;base64,' . base64_encode($wsdlContents), + $wsdlContents !== null ? 'data://text/plain;base64,' . base64_encode($wsdlContents) : null, $options ); $this->decoder = new ClientDecoder(); @@ -169,8 +182,9 @@ public function soapCall($name, $args) * * It returns the equivalent of PHP's * [`SoapClient::__getFunctions()`](http://php.net/manual/en/soapclient.getfunctions.php). + * In non-WSDL mode, this method returns `null`. * - * @return string[] + * @return string[]|null */ public function getFunctions() { @@ -182,8 +196,9 @@ public function getFunctions() * * It returns the equivalent of PHP's * [`SoapClient::__getTypes()`](http://php.net/manual/en/soapclient.gettypes.php). + * In non-WSDL mode, this method returns `null`. * - * @return string[] + * @return string[]|null */ public function getTypes() { @@ -215,8 +230,9 @@ public function getTypes() * assert('http://example.com/soap/service' === $client->getLocation(0)); * ``` * - * When the `location` option has been set in the `Client` constructor, this - * method returns the value of the given `location` option. + * When the `location` option has been set in the `Client` constructor + * (such as when in non-WSDL mode), this method returns the value of the + * given `location` option. * * Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. * diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 58c78d5..2572cfe 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -2,6 +2,8 @@ use Clue\React\Soap\Client; use PHPUnit\Framework\TestCase; +use React\Promise\Promise; +use Psr\Http\Message\RequestInterface; class ClientTest extends TestCase { @@ -16,4 +18,37 @@ public function testConstructorThrowsWhenUrlIsInvalid() $client = new Client($browser, $wsdl); } + + public function testNonWsdlClientReturnsSameLocationOptionForAnyFunction() + { + $browser = $this->getMockBuilder('Clue\React\Buzz\Browser')->disableOriginalConstructor()->getMock(); + + $client = new Client($browser, null, array('location' => 'http://example.com', 'uri' => 'http://example.com/uri')); + + $this->assertEquals('http://example.com', $client->getLocation('anything')); + } + + public function testNonWsdlClientReturnsNoTypesAndFunctions() + { + $browser = $this->getMockBuilder('Clue\React\Buzz\Browser')->disableOriginalConstructor()->getMock(); + + $client = new Client($browser, null, array('location' => 'http://example.com', 'uri' => 'http://example.com/uri')); + + $this->assertNull($client->getTypes()); + $this->assertNull($client->getFunctions()); + } + + public function testNonWsdlClientSendsPostRequestToGivenLocationForAnySoapCall() + { + $verify = function (RequestInterface $request) { + return ($request->getMethod() === 'POST' && (string)$request->getUri() === 'http://example.com'); + }; + $promise = new Promise(function () { }); + $browser = $this->getMockBuilder('Clue\React\Buzz\Browser')->disableOriginalConstructor()->getMock(); + $browser->expects($this->once())->method('send')->with($this->callback($verify))->willReturn($promise); + + $client = new Client($browser, null, array('location' => 'http://example.com', 'uri' => 'http://example.com/uri')); + + $client->soapCall('ping', array()); + } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 430095e..77bbce2 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -48,6 +48,25 @@ public function testBlzService() $this->assertInternalType('object', $result); } + public function testBlzServiceNonWsdlMode() + { + $this->client = new Client(new Browser($this->loop), null, array( + 'location' => 'http://www.thomas-bayer.com/axis2/services/BLZService', + 'uri' => 'http://thomas-bayer.com/blz/', + 'use' => SOAP_LITERAL + )); + + $api = new Proxy($this->client); + + // try encoding the "blz" parameter with the correct namespace (see uri) + // $promise = $api->getBank(new SoapParam('12070000', 'ns1:blz')); + $promise = $api->getBank(new SoapVar('12070000', XSD_STRING, null, null, 'blz', 'http://thomas-bayer.com/blz/')); + + $result = Block\await($promise, $this->loop); + + $this->assertInternalType('object', $result); + } + /** * @expectedException Exception */ diff --git a/tests/Protocol/ClientEncoderTest.php b/tests/Protocol/ClientEncoderTest.php index 9a750be..de3c9be 100644 --- a/tests/Protocol/ClientEncoderTest.php +++ b/tests/Protocol/ClientEncoderTest.php @@ -41,4 +41,25 @@ public function testConstructorThrowsWhenNonWsdlDoesNotDefineLocationAndUri() new ClientEncoder(null); } + + public function testEncodeRequestForBlzServiceNonWsdlMode() + { + $encoder = new ClientEncoder(null, array( + 'location' => 'http://www.thomas-bayer.com/axis2/services/BLZService', + 'uri' => 'http://thomas-bayer.com/blz/', + 'use' => SOAP_LITERAL + )); + + // try encoding the "blz" parameter with the correct namespace (see uri) + // $request = $encoder->encode('getBank', array(new SoapParam('12070000', 'ns1:blz'))); + $request = $encoder->encode('getBank', array( + new SoapVar('12070000', XSD_STRING, null, null, 'blz', 'http://thomas-bayer.com/blz/'), + )); + + $expected = ' +12070000 +'; + + $this->assertEquals($expected, (string)$request->getBody()); + } } From d1cfd4c82395f0edc273d8f82efa86ec8a97959e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 31 Aug 2018 22:30:57 +0200 Subject: [PATCH 3/3] Support SOAP 1.2 --- README.md | 9 +++++++++ src/Client.php | 9 +++++++++ src/Protocol/ClientDecoder.php | 2 +- src/Protocol/ClientEncoder.php | 18 +++++++++++++----- tests/FunctionalTest.php | 18 ++++++++++++++++++ tests/Protocol/ClientEncoderTest.php | 18 ++++++++++++++++++ 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d3f2f33..8720f83 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,15 @@ $client = new Client($browser, $wsdl, array( )); ``` +You can use the `soap_version` option to change from the default SOAP 1.1 to +use SOAP 1.2 instead: + +```php +$client = new Client($browser, $wsdl, array( + 'soap_version' => SOAP_1_2 +)); +``` + If you find an option is missing or not supported here, PRs are much appreciated! diff --git a/src/Client.php b/src/Client.php index 846ab24..da53727 100644 --- a/src/Client.php +++ b/src/Client.php @@ -111,6 +111,15 @@ * )); * ``` * + * You can use the `soap_version` option to change from the default SOAP 1.1 to + * use SOAP 1.2 instead: + * + * ```php + * $client = new Client($browser, $wsdl, array( + * 'soap_version' => SOAP_1_2 + * )); + * ``` + * * If you find an option is missing or not supported here, PRs are much * appreciated! * diff --git a/src/Protocol/ClientDecoder.php b/src/Protocol/ClientDecoder.php index 06a6999..7d3e61f 100644 --- a/src/Protocol/ClientDecoder.php +++ b/src/Protocol/ClientDecoder.php @@ -13,7 +13,7 @@ final class ClientDecoder extends SoapClient public function __construct() { - // to not pass actual WSDL to parent constructor + // do not pass actual WSDL to parent constructor // use faked non-wsdl-mode to let every method call pass through (pseudoCall) parent::__construct(null, array('location' => '1', 'uri' => '2')); } diff --git a/src/Protocol/ClientEncoder.php b/src/Protocol/ClientEncoder.php index e6433e8..e9d0018 100644 --- a/src/Protocol/ClientEncoder.php +++ b/src/Protocol/ClientEncoder.php @@ -44,14 +44,22 @@ public function encode($name, $args) */ public function __doRequest($request, $location, $action, $version, $one_way = 0) { + $headers = array(); + if ($version === SOAP_1_1) { + $headers = array( + 'SOAPAction' => $action, + 'Content-Type' => 'text/xml; charset=utf-8' + ); + } elseif ($version === SOAP_1_2) { + $headers = array( + 'Content-Type' => 'application/soap+xml; charset=utf-8; action=' . $action + ); + } + $this->request = new Request( 'POST', (string)$location, - array( - 'SOAPAction' => (string)$action, - 'Content-Type' => 'text/xml; charset=utf-8', - 'Content-Length' => strlen($request) - ), + $headers, (string)$request ); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 77bbce2..c43b8e3 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -48,6 +48,24 @@ public function testBlzService() $this->assertInternalType('object', $result); } + public function testBlzServiceWithSoapV12() + { + $this->client = new Client(new Browser($this->loop), self::$wsdl, array( + 'soap_version' => SOAP_1_2 + )); + + $this->assertCount(2, $this->client->getFunctions()); + $this->assertCount(3, $this->client->getTypes()); + + $api = new Proxy($this->client); + + $promise = $api->getBank(array('blz' => '12070000')); + + $result = Block\await($promise, $this->loop); + + $this->assertInternalType('object', $result); + } + public function testBlzServiceNonWsdlMode() { $this->client = new Client(new Browser($this->loop), null, array( diff --git a/tests/Protocol/ClientEncoderTest.php b/tests/Protocol/ClientEncoderTest.php index de3c9be..550d680 100644 --- a/tests/Protocol/ClientEncoderTest.php +++ b/tests/Protocol/ClientEncoderTest.php @@ -16,6 +16,24 @@ public function testEncodeCreatesRequestForNonWsdlRpcFunction() $this->assertSame('POST', $request->getMethod()); $this->assertSame('http://example.com/soap', (string)$request->getUri()); $this->assertSame('text/xml; charset=utf-8', $request->getHeaderLine('Content-Type')); + $this->assertSame('demo#add', $request->getHeaderLine('SOAPAction')); + } + + public function testEncodeCreatesRequestForNonWsdlRpcFunctionWithSoapV12() + { + $encoder = new ClientEncoder(null, array( + 'location' => 'http://example.com/soap', + 'uri' => 'demo', + 'soap_version' => SOAP_1_2 + )); + + $request = $encoder->encode('add', array('first' => 10, 'second' => 20)); + + $this->assertTrue($request instanceof RequestInterface); + $this->assertSame('POST', $request->getMethod()); + $this->assertSame('http://example.com/soap', (string)$request->getUri()); + $this->assertSame('application/soap+xml; charset=utf-8; action=demo#add', $request->getHeaderLine('Content-Type')); + $this->assertFalse($request->hasHeader('SOAPAction')); } /**