├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Client.php ├── Protocol ├── ClientDecoder.php └── ClientEncoder.php └── Proxy.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 (2020-10-28) 4 | 5 | * Feature / BC break: Update to reactphp/http v1.0.0. 6 | (#45 by @SimonFrings) 7 | 8 | * Feature / BC break: Add type declarations and require PHP 7.1+ as a consequence 9 | (#47 by @SimonFrings, #49 by @clue) 10 | 11 | * Use fully qualified class names in documentation. 12 | (#46 by @SimonFrings) 13 | 14 | * Improve test suite and add `.gitattributes` to exclude dev files from export. 15 | Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. 16 | (#40 by @andreybolonin, #42 and #44 by @SimonFrings and #48 by @clue) 17 | 18 | ## 1.0.0 (2018-11-07) 19 | 20 | * First stable release, now following SemVer! 21 | 22 | I'd like to thank [Bergfreunde GmbH](https://www.bergfreunde.de/), a German-based 23 | online retailer for Outdoor Gear & Clothing, for sponsoring large parts of this development! 🎉 24 | Thanks to sponsors like this, who understand the importance of open source 25 | development, I can justify spending time and focus on open source development 26 | instead of traditional paid work. 27 | 28 | > Did you know that I offer custom development services and issuing invoices for 29 | sponsorships of releases and for contributions? Contact me (@clue) for details. 30 | 31 | * BC break / Feature: Replace `Factory` with simplified `Client` constructor, 32 | add support for optional SOAP options and non-WSDL mode and 33 | respect WSDL type definitions when decoding and support classmap option. 34 | (#31, #32 and #33 by @clue) 35 | 36 | ```php 37 | // old 38 | $factory = new Factory($loop); 39 | $client = $factory->createClientFromWsdl($wsdl); 40 | 41 | // new 42 | $browser = new Browser($loop); 43 | $client = new Client($browser, $wsdl); 44 | ``` 45 | 46 | The `Client` constructor now accepts an array of options. All given options will 47 | be passed through to the underlying `SoapClient`. However, not all options 48 | make sense in this async implementation and as such may not have the desired 49 | effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php) 50 | documentation for more details. 51 | 52 | If working in WSDL mode, the `$options` parameter is optional. If working in 53 | non-WSDL mode, the WSDL parameter must be set to `null` and the options 54 | parameter must contain the `location` and `uri` options, where `location` is 55 | the URL of the SOAP server to send the request to, and `uri` is the target 56 | namespace of the SOAP service: 57 | 58 | ```php 59 | $client = new Client($browser, null, array( 60 | 'location' => 'http://example.com', 61 | 'uri' => 'http://ping.example.com', 62 | )); 63 | ``` 64 | 65 | * BC break: Mark all classes as final and all internal APIs as `@internal`. 66 | (#26 and #37 by @clue) 67 | 68 | * Feature: Add new `Client::withLocation()` method. 69 | (#38 by @floriansimon1, @pascal-hofmann and @clue) 70 | 71 | The `withLocation(string $location): self` method can be used to 72 | return a new `Client` with the updated location (URI) for all functions. 73 | 74 | Note that this is not to be confused with the WSDL file location. 75 | A WSDL file can contain any number of function definitions. 76 | It's very common that all of these functions use the same location definition. 77 | However, technically each function can potentially use a different location. 78 | 79 | ```php 80 | $client = $client->withLocation('http://example.com/soap'); 81 | 82 | assert('http://example.com/soap' === $client->getLocation('echo')); 83 | ``` 84 | 85 | As an alternative to this method, you can also set the `location` option 86 | in the `Client` constructor (such as when in non-WSDL mode). 87 | 88 | * Feature: Properly handle SOAP error responses, accept HTTP error responses and do not follow any HTTP redirects. 89 | (#35 by @clue) 90 | 91 | * Improve documentation and update project homepage, 92 | documentation for HTTP proxy servers, 93 | support timeouts for SOAP requests (HTTP timeout option) and 94 | add cancellation support. 95 | (#25, #29, #30 #34 and #36 by @clue) 96 | 97 | * Improve test suite by supporting PHPUnit 6, 98 | optionally skip functional integration tests requiring internet and 99 | test against PHP 7.2 and PHP 7.1 and latest ReactPHP components. 100 | (#24 by @carusogabriel and #27 and #28 by @clue) 101 | 102 | ## 0.2.0 (2017-10-02) 103 | 104 | * Feature: Added the possibility to use local WSDL files 105 | (#11 by @floriansimon1) 106 | 107 | ```php 108 | $factory = new Factory($loop); 109 | $wsdl = file_get_contents('service.wsdl'); 110 | $client = $factory->createClientFromWsdl($wsdl); 111 | ``` 112 | 113 | * Feature: Add `Client::getLocation()` helper 114 | (#13 by @clue) 115 | 116 | * Feature: Forward compatibility with clue/buzz-react v2.0 and upcoming EventLoop 117 | (#9 by @floriansimon1 and #19 and #21 by @clue) 118 | 119 | * Improve test suite by adding PHPUnit to require-dev and 120 | test PHP 5.3 through PHP 7.0 and HHVM and 121 | fix Travis build config 122 | (#1 by @WyriHaximus and #12, #17 and #22 by @clue) 123 | 124 | ## 0.1.0 (2014-07-28) 125 | 126 | * First tagged release 127 | 128 | ## 0.0.0 (2014-07-20) 129 | 130 | * Initial concept 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Christian Lück 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/reactphp-soap 2 | 3 | [![CI status](https://github.com/clue/reactphp-soap/workflows/CI/badge.svg)](https://github.com/clue/reactphp-soap/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/soap-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/soap-react) 5 | 6 | Simple, async [SOAP](https://en.wikipedia.org/wiki/SOAP) web service client library, 7 | built on top of [ReactPHP](https://reactphp.org/). 8 | 9 | Most notably, SOAP is often used for invoking 10 | [Remote procedure calls](https://en.wikipedia.org/wiki/Remote_procedure_call) (RPCs) 11 | in distributed systems. 12 | Internally, SOAP messages are encoded as XML and usually sent via HTTP POST requests. 13 | For the most part, SOAP (originally *Simple Object Access protocol*) is a protocol of the past, 14 | and in fact anything but *simple*. 15 | It is still in use by many (often *legacy*) systems. 16 | This project provides a *simple* API for invoking *async* RPCs to remote web services. 17 | 18 | * **Async execution of functions** - 19 | Send any number of functions (RPCs) to the remote web service in parallel and 20 | process their responses as soon as results come in. 21 | The Promise-based design provides a *sane* interface to working with out of order responses. 22 | * **Async processing of the WSDL** - 23 | The WSDL (web service description language) file will be downloaded and processed 24 | in the background. 25 | * **Event-driven core** - 26 | Internally, everything uses event handlers to react to incoming events, such as an incoming RPC result. 27 | * **Lightweight, SOLID design** - 28 | Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough) 29 | and does not get in your way. 30 | Built on top of tested components instead of re-inventing the wheel. 31 | * **Good test coverage** - 32 | Comes with an automated tests suite and is regularly tested against actual web services in the wild. 33 | 34 | **Table of contents** 35 | 36 | * [Support us](#support-us) 37 | * [Quickstart example](#quickstart-example) 38 | * [Usage](#usage) 39 | * [Client](#client) 40 | * [soapCall()](#soapcall) 41 | * [getFunctions()](#getfunctions) 42 | * [getTypes()](#gettypes) 43 | * [getLocation()](#getlocation) 44 | * [withLocation()](#withlocation) 45 | * [withHeaders()](#withheaders) 46 | * [Proxy](#proxy) 47 | * [Functions](#functions) 48 | * [Promises](#promises) 49 | * [Cancellation](#cancellation) 50 | * [Timeouts](#timeouts) 51 | * [Install](#install) 52 | * [Tests](#tests) 53 | * [License](#license) 54 | 55 | ## Support us 56 | 57 | We invest a lot of time developing, maintaining and updating our awesome 58 | open-source projects. You can help us sustain this high-quality of our work by 59 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 60 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 61 | for details. 62 | 63 | Let's take these projects to the next level together! 🚀 64 | 65 | ## Quickstart example 66 | 67 | Once [installed](#install), you can use the following code to query an example 68 | web service via SOAP: 69 | 70 | ```php 71 | get($wsdl)->then(function (Psr\Http\Message\ResponseInterface $response) use ($browser) { 79 | $client = new Clue\React\Soap\Client($browser, (string)$response->getBody()); 80 | $api = new Clue\React\Soap\Proxy($client); 81 | 82 | $api->getBank(array('blz' => '12070000'))->then(function ($result) { 83 | var_dump('Result', $result); 84 | }); 85 | }); 86 | ``` 87 | 88 | See also the [examples](examples). 89 | 90 | ## Usage 91 | 92 | ### Client 93 | 94 | The `Client` class is responsible for communication with the remote SOAP 95 | WebService server. It requires the WSDL file contents and an optional 96 | array of SOAP options: 97 | 98 | ```php 99 | $wsdl = ' '127.0.0.1', 116 | 'tcp' => array( 117 | 'bindto' => '192.168.10.1:0' 118 | ), 119 | 'tls' => array( 120 | 'verify_peer' => false, 121 | 'verify_peer_name' => false 122 | ) 123 | )); 124 | 125 | $browser = new React\Http\Browser($connector); 126 | $client = new Clue\React\Soap\Client($browser, $wsdl); 127 | ``` 128 | 129 | The `Client` works similar to PHP's `SoapClient` (which it uses under the 130 | hood), but leaves you the responsibility to load the WSDL file. This allows 131 | you to use local WSDL files, WSDL files from a cache or the most common form, 132 | downloading the WSDL file contents from an URL through the `Browser`: 133 | 134 | ```php 135 | $browser = new React\Http\Browser(); 136 | 137 | $browser->get($url)->then( 138 | function (Psr\Http\Message\ResponseInterface $response) use ($browser) { 139 | // WSDL file is ready, create client 140 | $client = new Clue\React\Soap\Client($browser, (string)$response->getBody()); 141 | 142 | // do something… 143 | }, 144 | function (Exception $e) { 145 | // an error occured while trying to download the WSDL 146 | } 147 | ); 148 | ``` 149 | 150 | The `Client` constructor loads the given WSDL file contents into memory and 151 | parses its definition. If the given WSDL file is invalid and can not be 152 | parsed, this will throw a `SoapFault`: 153 | 154 | ```php 155 | try { 156 | $client = new Clue\React\Soap\Client(null, $wsdl); 157 | } catch (SoapFault $e) { 158 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 159 | } 160 | ``` 161 | 162 | > Note that if you have an old version of `ext-xdebug` < 2.7 loaded, this may 163 | halt with a fatal error instead of throwing a `SoapFault`. It is not 164 | recommended to use this extension in production, so this should only ever 165 | affect test environments. 166 | 167 | The `Client` constructor accepts an array of options. All given options will 168 | be passed through to the underlying `SoapClient`. However, not all options 169 | make sense in this async implementation and as such may not have the desired 170 | effect. See also [`SoapClient`](https://www.php.net/manual/en/soapclient.soapclient.php) 171 | documentation for more details. 172 | 173 | If working in WSDL mode, the `$options` parameter is optional. If working in 174 | non-WSDL mode, the WSDL parameter must be set to `null` and the options 175 | parameter must contain the `location` and `uri` options, where `location` is 176 | the URL of the SOAP server to send the request to, and `uri` is the target 177 | namespace of the SOAP service: 178 | 179 | ```php 180 | $client = new Clue\React\Soap\Client(null, null, array( 181 | 'location' => 'http://example.com', 182 | 'uri' => 'http://ping.example.com', 183 | )); 184 | ``` 185 | 186 | Similarly, if working in WSDL mode, the `location` option can be used to 187 | explicitly overwrite the URL of the SOAP server to send the request to: 188 | 189 | ```php 190 | $client = new Clue\React\Soap\Client(null, $wsdl, array( 191 | 'location' => 'http://example.com' 192 | )); 193 | ``` 194 | 195 | You can use the `soap_version` option to change from the default SOAP 1.1 to 196 | use SOAP 1.2 instead: 197 | 198 | ```php 199 | $client = new Clue\React\Soap\Client(null, $wsdl, array( 200 | 'soap_version' => SOAP_1_2 201 | )); 202 | ``` 203 | 204 | You can use the `classmap` option to map certain WSDL types to PHP classes 205 | like this: 206 | 207 | ```php 208 | $client = new Clue\React\Soap\Client(null, $wsdl, array( 209 | 'classmap' => array( 210 | 'getBankResponseType' => BankResponse::class 211 | ) 212 | )); 213 | ``` 214 | 215 | The `proxy_host` option (and family) is not supported by this library. As an 216 | alternative, you can configure the given `$browser` instance to use an 217 | [HTTP proxy server](https://github.com/reactphp/http#http-proxy). 218 | If you find any other option is missing or not supported here, PRs are much 219 | appreciated! 220 | 221 | All public methods of the `Client` are considered *advanced usage*. 222 | If you want to call RPC functions, see below for the [`Proxy`](#proxy) class. 223 | 224 | #### soapCall() 225 | 226 | The `soapCall(string $method, mixed[] $arguments): PromiseInterface` method can be used to 227 | queue the given function to be sent via SOAP and wait for a response from the remote web service. 228 | 229 | ```php 230 | // advanced usage, see Proxy for recommended alternative 231 | $promise = $client->soapCall('ping', array('hello', 42)); 232 | ``` 233 | 234 | Note: This is considered *advanced usage*, you may want to look into using the [`Proxy`](#proxy) instead. 235 | 236 | ```php 237 | $proxy = new Clue\React\Soap\Proxy($client); 238 | $promise = $proxy->ping('hello', 42); 239 | ``` 240 | 241 | #### getFunctions() 242 | 243 | The `getFunctions(): string[]|null` method can be used to 244 | return an array of functions defined in the WSDL. 245 | 246 | It returns the equivalent of PHP's 247 | [`SoapClient::__getFunctions()`](https://www.php.net/manual/en/soapclient.getfunctions.php). 248 | In non-WSDL mode, this method returns `null`. 249 | 250 | #### getTypes() 251 | 252 | The `getTypes(): string[]|null` method can be used to 253 | return an array of types defined in the WSDL. 254 | 255 | It returns the equivalent of PHP's 256 | [`SoapClient::__getTypes()`](https://www.php.net/manual/en/soapclient.gettypes.php). 257 | In non-WSDL mode, this method returns `null`. 258 | 259 | #### getLocation() 260 | 261 | The `getLocation(string|int $function): string` method can be used to 262 | return the location (URI) of the given webservice `$function`. 263 | 264 | Note that this is not to be confused with the WSDL file location. 265 | A WSDL file can contain any number of function definitions. 266 | It's very common that all of these functions use the same location definition. 267 | However, technically each function can potentially use a different location. 268 | 269 | The `$function` parameter should be a string with the the SOAP function name. 270 | See also [`getFunctions()`](#getfunctions) for a list of all available functions. 271 | 272 | ```php 273 | assert('http://example.com/soap/service' === $client->getLocation('echo')); 274 | ``` 275 | 276 | For easier access, this function also accepts a numeric function index. 277 | It then uses [`getFunctions()`](#getfunctions) internally to get the function 278 | name for the given index. 279 | This is particularly useful for the very common case where all functions use the 280 | same location and accessing the first location is sufficient. 281 | 282 | ```php 283 | assert('http://example.com/soap/service' === $client->getLocation(0)); 284 | ``` 285 | 286 | When the `location` option has been set in the `Client` constructor 287 | (such as when in non-WSDL mode) or via the `withLocation()` method, this 288 | method returns the value of the given location. 289 | 290 | Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. 291 | 292 | #### withLocation() 293 | 294 | The `withLocation(string $location): self` method can be used to 295 | return a new `Client` with the updated location (URI) for all functions. 296 | 297 | Note that this is not to be confused with the WSDL file location. 298 | A WSDL file can contain any number of function definitions. 299 | It's very common that all of these functions use the same location definition. 300 | However, technically each function can potentially use a different location. 301 | 302 | ```php 303 | $client = $client->withLocation('http://example.com/soap'); 304 | 305 | assert('http://example.com/soap' === $client->getLocation('echo')); 306 | ``` 307 | 308 | As an alternative to this method, you can also set the `location` option 309 | in the `Client` constructor (such as when in non-WSDL mode). 310 | 311 | #### withHeaders() 312 | 313 | The `withHeaders(array $headers): self` method can be used to 314 | return a new `Client` with the updated headers for all functions. 315 | This allows to set specific headers required by some SOAP endpoints, like 316 | for authentication, etc. 317 | 318 | ```php 319 | $client = $client->withHeaders([new SoapHeader(...)]); 320 | ``` 321 | 322 | ### Proxy 323 | 324 | The `Proxy` class wraps an existing [`Client`](#client) instance in order to ease calling 325 | SOAP functions. 326 | 327 | ```php 328 | $proxy = new Clue\React\Soap\Proxy($client); 329 | ``` 330 | 331 | > Note that this class is called "Proxy" because it will forward (proxy) all 332 | method calls to the actual SOAP service via the underlying 333 | [`Client::soapCall()`](#soapcall) method. This is not to be confused with 334 | using a proxy server. See [`Client`](#client) documentation for more 335 | details on how to use an HTTP proxy server. 336 | 337 | #### Functions 338 | 339 | Each and every method call to the `Proxy` class will be sent via SOAP. 340 | 341 | ```php 342 | $proxy->myMethod($myArg1, $myArg2)->then(function ($response) { 343 | // result received 344 | }); 345 | ``` 346 | 347 | Please refer to your WSDL or its accompanying documentation for details 348 | on which functions and arguments are supported. 349 | 350 | #### Promises 351 | 352 | Issuing SOAP functions is async (non-blocking), so you can actually send multiple RPC requests in parallel. 353 | The web service will respond to each request with a return value. The order is not guaranteed. 354 | Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a request is *fulfilled* 355 | (i.e. either successfully resolved or rejected with an error): 356 | 357 | ```php 358 | $proxy->demo()->then( 359 | function ($response) { 360 | // response received for demo function 361 | }, 362 | function (Exception $e) { 363 | // an error occured while executing the request 364 | } 365 | }); 366 | ``` 367 | 368 | #### Cancellation 369 | 370 | The returned Promise is implemented in such a way that it can be cancelled 371 | when it is still pending. 372 | Cancelling a pending promise will reject its value with an Exception and 373 | clean up any underlying resources. 374 | 375 | ```php 376 | $promise = $proxy->demo(); 377 | 378 | Loop::addTimer(2.0, function () use ($promise) { 379 | $promise->cancel(); 380 | }); 381 | ``` 382 | 383 | #### Timeouts 384 | 385 | This library uses a very efficient HTTP implementation, so most SOAP requests 386 | should usually be completed in mere milliseconds. However, when sending SOAP 387 | requests over an unreliable network (the internet), there are a number of things 388 | that can go wrong and may cause the request to fail after a time. As such, 389 | timeouts are handled by the underlying HTTP library and this library respects 390 | PHP's `default_socket_timeout` setting (default 60s) as a timeout for sending the 391 | outgoing SOAP request and waiting for a successful response and will otherwise 392 | cancel the pending request and reject its value with an Exception. 393 | 394 | Note that this timeout value covers creating the underlying transport connection, 395 | sending the SOAP request, waiting for the remote service to process the request 396 | and receiving the full SOAP response. To use a custom timeout value, you can 397 | pass the timeout to the [underlying `Browser`](https://github.com/reactphp/http#timeouts) 398 | like this: 399 | 400 | ```php 401 | $browser = new React\Http\Browser(); 402 | $browser = $browser->withTimeout(10.0); 403 | 404 | $client = new Clue\React\Soap\Client($browser, $wsdl); 405 | $proxy = new Clue\React\Soap\Proxy($client); 406 | 407 | $proxy->demo()->then(function ($response) { 408 | // response received within 10 seconds maximum 409 | var_dump($response); 410 | }); 411 | ``` 412 | 413 | Similarly, you can use a negative timeout value to not apply a timeout at all 414 | or use a `null` value to restore the default handling. Note that the underlying 415 | connection may still impose a different timeout value. See also the underlying 416 | [timeouts documentation](https://github.com/reactphp/http#timeouts) for more details. 417 | 418 | ## Install 419 | 420 | The recommended way to install this library is [through Composer](https://getcomposer.org). 421 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 422 | 423 | This project follows [SemVer](https://semver.org/). 424 | This will install the latest supported version: 425 | 426 | ```bash 427 | $ composer require clue/soap-react:^2.0 428 | ``` 429 | 430 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 431 | 432 | This project aims to run on any platform and thus only requires `ext-soap` and 433 | supports running on PHP 7.1+. 434 | 435 | ## Tests 436 | 437 | To run the test suite, you first need to clone this repo and then install all 438 | dependencies [through Composer](https://getcomposer.org): 439 | 440 | ```bash 441 | $ composer install 442 | ``` 443 | 444 | To run the test suite, go to the project root and run: 445 | 446 | ```bash 447 | $ php vendor/bin/phpunit 448 | ``` 449 | 450 | The test suite also contains a number of functional integration tests that rely 451 | on a stable internet connection. 452 | If you do not want to run these, they can simply be skipped like this: 453 | 454 | ```bash 455 | $ php vendor/bin/phpunit --exclude-group internet 456 | ``` 457 | 458 | ## License 459 | 460 | This project is released under the permissive [MIT license](LICENSE). 461 | 462 | > Did you know that I offer custom development services and issuing invoices for 463 | sponsorships of releases and for contributions? Contact me (@clue) for details. 464 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/soap-react", 3 | "description": "Simple, async SOAP webservice client library, built on top of ReactPHP", 4 | "keywords": ["SOAP", "SoapClient", "WebService", "WSDL", "ReactPHP"], 5 | "homepage": "https://github.com/clue/reactphp-soap", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@clue.engineering" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { "Clue\\React\\Soap\\": "src/" } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { "Clue\\Tests\\React\\Soap\\": "tests/" } 18 | }, 19 | "require": { 20 | "php": ">=7.1", 21 | "react/http": "^1.5", 22 | "react/promise": "^2.1 || ^1.2", 23 | "ext-soap": "*" 24 | }, 25 | "require-dev": { 26 | "clue/block-react": "^1.0", 27 | "phpunit/phpunit": "^9.3 || ^7.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 35 | * 'tcp' => array( 36 | * 'bindto' => '192.168.10.1:0' 37 | * ), 38 | * 'tls' => array( 39 | * 'verify_peer' => false, 40 | * 'verify_peer_name' => false 41 | * ) 42 | * )); 43 | * 44 | * $browser = new React\Http\Browser($connector); 45 | * $client = new Clue\React\Soap\Client($browser, $wsdl); 46 | * ``` 47 | * 48 | * The `Client` works similar to PHP's `SoapClient` (which it uses under the 49 | * hood), but leaves you the responsibility to load the WSDL file. This allows 50 | * you to use local WSDL files, WSDL files from a cache or the most common form, 51 | * downloading the WSDL file contents from an URL through the `Browser`: 52 | * 53 | * ```php 54 | * $browser = new React\Http\Browser(); 55 | * 56 | * $browser->get($url)->then( 57 | * function (Psr\Http\Message\ResponseInterface $response) use ($browser) { 58 | * // WSDL file is ready, create client 59 | * $client = new Clue\React\Soap\Client($browser, (string)$response->getBody()); 60 | * 61 | * // do something… 62 | * }, 63 | * function (Exception $e) { 64 | * // an error occured while trying to download the WSDL 65 | * } 66 | * ); 67 | * ``` 68 | * 69 | * The `Client` constructor loads the given WSDL file contents into memory and 70 | * parses its definition. If the given WSDL file is invalid and can not be 71 | * parsed, this will throw a `SoapFault`: 72 | * 73 | * ```php 74 | * try { 75 | * $client = new Clue\React\Soap\Client(null, $wsdl); 76 | * } catch (SoapFault $e) { 77 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 78 | * } 79 | * ``` 80 | * 81 | * > Note that if you have an old version of `ext-xdebug` < 2.7 loaded, this may 82 | * halt with a fatal error instead of throwing a `SoapFault`. It is not 83 | * recommended to use this extension in production, so this should only ever 84 | * affect test environments. 85 | * 86 | * The `Client` constructor accepts an array of options. All given options will 87 | * be passed through to the underlying `SoapClient`. However, not all options 88 | * make sense in this async implementation and as such may not have the desired 89 | * effect. See also [`SoapClient`](https://www.php.net/manual/en/soapclient.soapclient.php) 90 | * documentation for more details. 91 | * 92 | * If working in WSDL mode, the `$options` parameter is optional. If working in 93 | * non-WSDL mode, the WSDL parameter must be set to `null` and the options 94 | * parameter must contain the `location` and `uri` options, where `location` is 95 | * the URL of the SOAP server to send the request to, and `uri` is the target 96 | * namespace of the SOAP service: 97 | * 98 | * ```php 99 | * $client = new Clue\React\Soap\Client(null, null, array( 100 | * 'location' => 'http://example.com', 101 | * 'uri' => 'http://ping.example.com', 102 | * )); 103 | * ``` 104 | * 105 | * Similarly, if working in WSDL mode, the `location` option can be used to 106 | * explicitly overwrite the URL of the SOAP server to send the request to: 107 | * 108 | * ```php 109 | * $client = new Clue\React\Soap\Client(null, $wsdl, array( 110 | * 'location' => 'http://example.com' 111 | * )); 112 | * ``` 113 | * 114 | * You can use the `soap_version` option to change from the default SOAP 1.1 to 115 | * use SOAP 1.2 instead: 116 | * 117 | * ```php 118 | * $client = new Clue\React\Soap\Client(null, $wsdl, array( 119 | * 'soap_version' => SOAP_1_2 120 | * )); 121 | * ``` 122 | * 123 | * You can use the `classmap` option to map certain WSDL types to PHP classes 124 | * like this: 125 | * 126 | * ```php 127 | * $client = new Clue\React\Soap\Client(null, $wsdl, array( 128 | * 'classmap' => array( 129 | * 'getBankResponseType' => BankResponse::class 130 | * ) 131 | * )); 132 | * ``` 133 | * 134 | * The `proxy_host` option (and family) is not supported by this library. As an 135 | * alternative, you can configure the given `$browser` instance to use an 136 | * [HTTP proxy server](https://github.com/clue/reactphp/http#http-proxy). 137 | * If you find any other option is missing or not supported here, PRs are much 138 | * appreciated! 139 | * 140 | * All public methods of the `Client` are considered *advanced usage*. 141 | * If you want to call RPC functions, see below for the [`Proxy`](#proxy) class. 142 | */ 143 | class Client 144 | { 145 | /** @var Browser */ 146 | private $browser; 147 | 148 | private $encoder; 149 | private $decoder; 150 | 151 | /** 152 | * Instantiate a new SOAP client for the given WSDL contents. 153 | * 154 | * @param ?Browser $browser 155 | * @param ?string $wsdlContents 156 | * @param ?array $options 157 | */ 158 | public function __construct(?Browser $browser, ?string $wsdlContents, array $options = array()) 159 | { 160 | $wsdl = $wsdlContents !== null ? 'data://text/plain;base64,' . base64_encode($wsdlContents) : null; 161 | 162 | $this->browser = $browser ?? new Browser(); 163 | 164 | // Accept HTTP responses with error status codes as valid responses. 165 | // This is done in order to process these error responses through the normal SOAP decoder. 166 | // Additionally, we explicitly limit number of redirects to zero because following redirects makes little sense 167 | // because it transforms the POST request to a GET one and hence loses the SOAP request body. 168 | $this->browser = $this->browser->withRejectErrorResponse(false); 169 | $this->browser = $this->browser->withFollowRedirects(0); 170 | 171 | $this->encoder = new ClientEncoder($wsdl, $options); 172 | $this->decoder = new ClientDecoder($wsdl, $options); 173 | } 174 | 175 | /** 176 | * Queue the given function to be sent via SOAP and wait for a response from the remote web service. 177 | * 178 | * ```php 179 | * // advanced usage, see Proxy for recommended alternative 180 | * $promise = $client->soapCall('ping', array('hello', 42)); 181 | * ``` 182 | * 183 | * Note: This is considered *advanced usage*, you may want to look into using the [`Proxy`](#proxy) instead. 184 | * 185 | * ```php 186 | * $proxy = new Clue\React\Soap\Proxy($client); 187 | * $promise = $proxy->ping('hello', 42); 188 | * ``` 189 | * 190 | * @param string $name 191 | * @param mixed[] $args 192 | * @return PromiseInterface Returns a Promise 193 | */ 194 | public function soapCall(string $name, array $args): PromiseInterface 195 | { 196 | try { 197 | $request = $this->encoder->encode($name, $args); 198 | } catch (\Exception $e) { 199 | $deferred = new Deferred(); 200 | $deferred->reject($e); 201 | return $deferred->promise(); 202 | } 203 | 204 | $decoder = $this->decoder; 205 | 206 | return $this->browser->request( 207 | $request->getMethod(), 208 | (string) $request->getUri(), 209 | $request->getHeaders(), 210 | (string) $request->getBody() 211 | )->then( 212 | function (ResponseInterface $response) use ($decoder, $name) { 213 | // HTTP response received => decode results for this function call 214 | return $decoder->decode($name, (string)$response->getBody()); 215 | } 216 | ); 217 | } 218 | 219 | /** 220 | * Returns an array of functions defined in the WSDL. 221 | * 222 | * It returns the equivalent of PHP's 223 | * [`SoapClient::__getFunctions()`](https://www.php.net/manual/en/soapclient.getfunctions.php). 224 | * In non-WSDL mode, this method returns `null`. 225 | * 226 | * @return string[]|null 227 | */ 228 | public function getFunctions(): ?array 229 | { 230 | return $this->encoder->__getFunctions(); 231 | } 232 | 233 | /** 234 | * Returns an array of types defined in the WSDL. 235 | * 236 | * It returns the equivalent of PHP's 237 | * [`SoapClient::__getTypes()`](https://www.php.net/manual/en/soapclient.gettypes.php). 238 | * In non-WSDL mode, this method returns `null`. 239 | * 240 | * @return string[]|null 241 | */ 242 | public function getTypes(): ?array 243 | { 244 | return $this->encoder->__getTypes(); 245 | } 246 | 247 | /** 248 | * Returns the location (URI) of the given webservice `$function`. 249 | * 250 | * Note that this is not to be confused with the WSDL file location. 251 | * A WSDL file can contain any number of function definitions. 252 | * It's very common that all of these functions use the same location definition. 253 | * However, technically each function can potentially use a different location. 254 | * 255 | * The `$function` parameter should be a string with the the SOAP function name. 256 | * See also [`getFunctions()`](#getfunctions) for a list of all available functions. 257 | * 258 | * ```php 259 | * assert('http://example.com/soap/service' === $client->getLocation('echo')); 260 | * ``` 261 | * 262 | * For easier access, this function also accepts a numeric function index. 263 | * It then uses [`getFunctions()`](#getfunctions) internally to get the function 264 | * name for the given index. 265 | * This is particularly useful for the very common case where all functions use the 266 | * same location and accessing the first location is sufficient. 267 | * 268 | * ```php 269 | * assert('http://example.com/soap/service' === $client->getLocation(0)); 270 | * ``` 271 | * 272 | * When the `location` option has been set in the `Client` constructor 273 | * (such as when in non-WSDL mode) or via the `withLocation()` method, this 274 | * method returns the value of the given location. 275 | * 276 | * Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. 277 | * 278 | * @param string|int $function 279 | * @return string 280 | * @throws \SoapFault if given function does not exist 281 | * @see self::getFunctions() 282 | */ 283 | public function getLocation($function): string 284 | { 285 | if (is_int($function)) { 286 | $functions = $this->getFunctions(); 287 | if (isset($functions[$function]) && preg_match('/^\w+ (\w+)\(/', $functions[$function], $match)) { 288 | $function = $match[1]; 289 | } 290 | } 291 | 292 | // encode request for given $function 293 | return (string)$this->encoder->encode($function, array())->getUri(); 294 | } 295 | 296 | /** 297 | * Returns a new `Client` with the updated location (URI) for all functions. 298 | * 299 | * Note that this is not to be confused with the WSDL file location. 300 | * A WSDL file can contain any number of function definitions. 301 | * It's very common that all of these functions use the same location definition. 302 | * However, technically each function can potentially use a different location. 303 | * 304 | * ```php 305 | * $client = $client->withLocation('http://example.com/soap'); 306 | * 307 | * assert('http://example.com/soap' === $client->getLocation('echo')); 308 | * ``` 309 | * 310 | * As an alternative to this method, you can also set the `location` option 311 | * in the `Client` constructor (such as when in non-WSDL mode). 312 | * 313 | * @param string $location 314 | * @return self 315 | * @see self::getLocation() 316 | */ 317 | public function withLocation(string $location): self 318 | { 319 | $client = clone $this; 320 | $client->encoder = clone $this->encoder; 321 | $client->encoder->__setLocation($location); 322 | 323 | return $client; 324 | } 325 | 326 | /** 327 | * Returns a new `Client` with the given headers for all functions. 328 | * 329 | * @param array $headers 330 | * @return self 331 | */ 332 | public function withHeaders(array $headers): self 333 | { 334 | $client = clone $this; 335 | $client->encoder = clone $this->encoder; 336 | $client->encoder->__setSoapHeaders($headers); 337 | 338 | return $client; 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/Protocol/ClientDecoder.php: -------------------------------------------------------------------------------- 1 | response = $response; 24 | 25 | // Let's pretend we just invoked the given SOAP function. 26 | // This won't actually invoke anything (see `__doRequest()`), but this 27 | // requires a valid function name to match its definition in the WSDL. 28 | // Internally, simply use the injected response to parse its results. 29 | $ret = $this->__soapCall($function, array()); 30 | $this->response = null; 31 | 32 | return $ret; 33 | } 34 | 35 | /** 36 | * Overwrites the internal request logic to parse the response 37 | * 38 | * By overwriting this method, we can skip the actual request sending logic 39 | * and still use the internal parsing logic by injecting the response as 40 | * the return code in this method. This will implicitly be invoked by the 41 | * call to `pseudoCall()` in the above `decode()` method. 42 | * 43 | * @see \SoapClient::__doRequest() 44 | */ 45 | public function __doRequest($request, $location, $action, $version, $one_way = 0) 46 | { 47 | // the actual result doesn't actually matter, just return the given result 48 | // this will be processed internally and will return the parsed result 49 | return $this->response; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Protocol/ClientEncoder.php: -------------------------------------------------------------------------------- 1 | __soapCall($name, $args); 26 | 27 | $request = $this->request; 28 | $this->request = null; 29 | 30 | return $request; 31 | } 32 | 33 | /** 34 | * Overwrites the internal request logic to build the request message 35 | * 36 | * By overwriting this method, we can skip the actual request sending logic 37 | * and still use the internal request serializing logic by accessing the 38 | * given `$request` parameter and building our custom request object from 39 | * it. We skip/ignore its parsing logic by returing an empty response here. 40 | * This will implicitly be invoked by the call to `__soapCall()` in the 41 | * above `encode()` method. 42 | * 43 | * @see \SoapClient::__doRequest() 44 | */ 45 | public function __doRequest($request, $location, $action, $version, $one_way = 0) 46 | { 47 | $headers = array(); 48 | if ($version === SOAP_1_1) { 49 | $headers = array( 50 | 'SOAPAction' => $action, 51 | 'Content-Type' => 'text/xml; charset=utf-8' 52 | ); 53 | } elseif ($version === SOAP_1_2) { 54 | $headers = array( 55 | 'Content-Type' => 'application/soap+xml; charset=utf-8; action=' . $action 56 | ); 57 | } 58 | 59 | $this->request = new Request( 60 | 'POST', 61 | (string)$location, 62 | $headers, 63 | (string)$request 64 | ); 65 | 66 | // do not actually block here, just pretend we're done... 67 | return ''; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Proxy.php: -------------------------------------------------------------------------------- 1 | myMethod($myArg1, $myArg2)->then(function ($response) { 19 | * // result received 20 | * }); 21 | * ``` 22 | * 23 | * Please refer to your WSDL or its accompanying documentation for details 24 | * on which functions and arguments are supported. 25 | * 26 | * > Note that this class is called "Proxy" because it will forward (proxy) all 27 | * method calls to the actual SOAP service via the underlying 28 | * [`Client::soapCall()`](#soapcall) method. This is not to be confused with 29 | * using a proxy server. See [`Client`](#client) documentation for more 30 | * details on how to use an HTTP proxy server. 31 | */ 32 | final class Proxy 33 | { 34 | private $client; 35 | 36 | public function __construct(Client $client) 37 | { 38 | $this->client = $client; 39 | } 40 | 41 | /** 42 | * @param string $name 43 | * @param mixed[] $args 44 | * @return PromiseInterface 45 | */ 46 | public function __call(string $name, array $args): PromiseInterface 47 | { 48 | return $this->client->soapCall($name, $args); 49 | } 50 | } 51 | --------------------------------------------------------------------------------