├── .gitignore
├── phpunit.xml
├── composer.json
├── LICENSE
├── .github
└── workflows
│ └── main.yaml
├── tests
├── unit
│ ├── FactoryTest.php
│ ├── example.wsdl
│ └── SoapClientTest.php
└── functional
│ └── SoapClientTest.php
├── README.md
└── src
├── SoapClient.php
└── Factory.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .idea/
3 | .DS_Store
4 | /build/
5 | composer.lock
6 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tests/unit
5 |
6 |
7 |
8 |
9 | src
10 |
11 |
12 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meng-tian/async-soap-guzzle",
3 | "description": "An asynchronous SOAP client build on top of Guzzle.",
4 | "keywords": ["SOAP", "asynchronous", "Guzzle"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Meng Tian",
9 | "email": "tianmeng94@hotmail.com"
10 | }
11 | ],
12 | "require": {
13 | "php": ">=7.1.0",
14 | "meng-tian/php-async-soap": "~1.0",
15 | "meng-tian/soap-http-binding": "~0.4.0",
16 | "psr/http-factory": "~1.0",
17 | "guzzlehttp/guzzle": "^6.1 || ^7.0"
18 | },
19 | "require-dev": {
20 | "phpunit/phpunit": "~7.0|~9.3",
21 | "laminas/laminas-diactoros": "^2.0"
22 | },
23 | "autoload": {
24 | "psr-4": {"Meng\\AsyncSoap\\Guzzle\\": "src/"}
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Meng Tian
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
10 | furnished 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | strategy:
8 | matrix:
9 | operating-system: [ubuntu-latest]
10 | php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
11 |
12 | runs-on: ${{ matrix.operating-system }}
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v2
17 |
18 | - name: Setup PHP, with composer and extensions
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: ${{ matrix.php-versions }}
22 |
23 | - name: Get composer cache directory
24 | id: composer-cache
25 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
26 |
27 | - name: Cache composer dependencies
28 | uses: actions/cache@v2
29 | with:
30 | path: ${{ steps.composer-cache.outputs.dir }}
31 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
32 | restore-keys: ${{ runner.os }}-composer-
33 |
34 | - name: Install Composer dependencies
35 | run: |
36 | composer install --no-progress --prefer-dist --optimize-autoloader
37 |
38 | - name: Run Tests
39 | run: vendor/bin/phpunit --coverage-text
40 |
--------------------------------------------------------------------------------
/tests/unit/FactoryTest.php:
--------------------------------------------------------------------------------
1 | create(new Client(), new StreamFactory(), new RequestFactory(), null, ['uri'=>'', 'location'=>'']);
22 |
23 | $this->assertTrue($client instanceof SoapClient);
24 | }
25 |
26 | /**
27 | * @test
28 | */
29 | public function wsdlFromHttpUrl()
30 | {
31 | $handlerMock = new MockHandler([
32 | new Response('200', [], fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'example.wsdl', 'r'))
33 | ]);
34 | $handler = new HandlerStack($handlerMock);
35 | $clientMock = new Client(['handler' => $handler]);
36 |
37 | $factory = new Factory();
38 | $client = $factory->create($clientMock, new StreamFactory, new RequestFactory, 'http://www.mysite.com/wsdl');
39 |
40 | $this->assertTrue($client instanceof SoapClient);
41 | }
42 |
43 | /**
44 | * @test
45 | */
46 | public function wsdlFromLocalFile()
47 | {
48 | $factory = new Factory();
49 | $client = $factory->create(new Client(), new StreamFactory(), new RequestFactory(), dirname(__FILE__) . DIRECTORY_SEPARATOR . 'example.wsdl');
50 |
51 | $this->assertTrue($client instanceof SoapClient);
52 | }
53 |
54 | /**
55 | * @test
56 | */
57 | public function wsdlFromDataUri()
58 | {
59 | $wsdlString = file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'example.wsdl');
60 | $wsdl = 'data://text/plain;base64,' . base64_encode($wsdlString);
61 |
62 | $factory = new Factory();
63 | $client = $factory->create(new Client(), new StreamFactory(), new RequestFactory(), $wsdl);
64 |
65 | $this->assertTrue($client instanceof SoapClient);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/unit/example.wsdl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
31 |
32 |
34 |
35 |
37 |
38 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 | snowboarding-info.com Endorsement Service
51 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Asynchronous SOAP client
2 |
3 | [](https://codecov.io/github/meng-tian/async-soap-guzzle?branch=master) 
4 |
5 | An asynchronous SOAP client build on top of Guzzle. The `SoapClient` implements [meng-tian/php-async-soap](https://github.com/meng-tian/php-async-soap).
6 |
7 | ## Requirement
8 | PHP 7.1 --enablelibxml --enable-soap
9 |
10 | ## Install
11 | ```
12 | composer require meng-tian/async-soap-guzzle
13 | ```
14 |
15 | ## Usage
16 | From [v0.4.0](https://github.com/meng-tian/async-soap-guzzle/tree/v0.4.0) or newer, an instance of `Psr\Http\Message\RequestFactoryInterface` and an instance of `Psr\Http\Message\StreamFactoryInterface` need to be injected into `Meng\AsyncSoap\Guzzle\Factory`. These two interfaces are defined in [PSR-17](https://www.php-fig.org/psr/psr-17/) to create [PSR-7](https://www.php-fig.org/psr/psr-7/) compliant HTTP instances. This change will decouple this library from any specific implementation of PSR-7 and PSR-17. Clients can determine which implementation of PSR-17 they want to use. Plenty of different implementations of PSR17 can be found from [Packagist](https://packagist.org/?query=psr-17), e.g., `symfony/psr-http-message-bridge`, or `laminas/laminas-diactoros`.
17 |
18 | 1. Require this library and an implementation of PSR-17 in your `composer.json`:
19 | ```json
20 | ...
21 | "require": {
22 | "php": ">=7.1.0",
23 | "meng-tian/async-soap-guzzle": "~0.4.0",
24 | "laminas/laminas-diactoros": "^2.0" # this can be replaced by any implementation of PSR-17
25 | },
26 | ...
27 | ```
28 | 2. Run `composer install`
29 |
30 | 3. Create your async SOAP client and call your SOAP messages:
31 | ```php
32 | use GuzzleHttp\Client;
33 | use Meng\AsyncSoap\Guzzle\Factory;
34 | use Laminas\Diactoros\RequestFactory;
35 | use Laminas\Diactoros\StreamFactory;
36 |
37 | $factory = new Factory();
38 | $client = $factory->create(new Client(), new StreamFactory(), new RequestFactory(), 'http://www.webservicex.net/Statistics.asmx?WSDL');
39 |
40 | // async call
41 | $promise = $client->callAsync('GetStatistics', [['X' => [1,2,3]]]);
42 | $result = $promise->wait();
43 |
44 | // sync call
45 | $result = $client->call('GetStatistics', [['X' => [1,2,3]]]);
46 |
47 | // magic method
48 | $promise = $client->GetStatistics(['X' => [1,2,3]]);
49 | $result = $promise->wait();
50 | ```
51 |
52 | ## License
53 | This library is released under [MIT](https://github.com/meng-tian/async-soap-guzzle/blob/master/LICENSE) license.
54 |
--------------------------------------------------------------------------------
/src/SoapClient.php:
--------------------------------------------------------------------------------
1 | httpBindingPromise = $httpBindingPromise;
20 | $this->client = $client;
21 | }
22 |
23 | public function __call($name, $arguments)
24 | {
25 | return $this->callAsync($name, $arguments);
26 | }
27 |
28 | public function call($name, array $arguments, array $options = null, $inputHeaders = null, array &$outputHeaders = null)
29 | {
30 | $callPromise = $this->callAsync($name, $arguments, $options, $inputHeaders, $outputHeaders);
31 | return $callPromise->wait();
32 | }
33 |
34 | public function callAsync($name, array $arguments, array $options = null, $inputHeaders = null, array &$outputHeaders = null)
35 | {
36 | return \GuzzleHttp\Promise\Coroutine::of(
37 | function () use ($name, $arguments, $options, $inputHeaders, &$outputHeaders) {
38 | /** @var HttpBinding $httpBinding */
39 | $httpBinding = (yield $this->httpBindingPromise);
40 | $request = $httpBinding->request($name, $arguments, $options, $inputHeaders);
41 | $requestOptions = isset($options['request_options']) ? $options['request_options'] : [];
42 |
43 | try {
44 | $response = (yield $this->client->sendAsync($request, $requestOptions));
45 | yield $this->interpretResponse($httpBinding, $response, $name, $outputHeaders);
46 | } catch (RequestException $exception) {
47 | if ($exception->hasResponse()) {
48 | $response = $exception->getResponse();
49 | yield $this->interpretResponse($httpBinding, $response, $name, $outputHeaders);
50 | } else {
51 | throw $exception;
52 | }
53 | } finally {
54 | $request->getBody()->close();
55 | }
56 | }
57 | );
58 | }
59 |
60 | private function interpretResponse(HttpBinding $httpBinding, ResponseInterface $response, $name, &$outputHeaders)
61 | {
62 | try {
63 | return $httpBinding->response($response, $name, $outputHeaders);
64 | } finally {
65 | $response->getBody()->close();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Factory.php:
--------------------------------------------------------------------------------
1 | isHttpUrl($wsdl)) {
35 | $httpBindingPromise = $client->requestAsync('GET', $wsdl)->then(
36 | function (ResponseInterface $response) use ($streamFactory, $requestFactory, $options) {
37 | $wsdl = $response->getBody()->__toString();
38 | $interpreter = new Interpreter('data://text/plain;base64,' . base64_encode($wsdl), $options);
39 | return new HttpBinding($interpreter, new RequestBuilder($streamFactory, $requestFactory), $streamFactory);
40 | }
41 | );
42 | } else {
43 | $httpBindingPromise = new FulfilledPromise(
44 | new HttpBinding(new Interpreter($wsdl, $options), new RequestBuilder($streamFactory, $requestFactory), $streamFactory)
45 | );
46 | }
47 |
48 | return new SoapClient($client, $httpBindingPromise);
49 | }
50 |
51 | private function isHttpUrl($wsdl)
52 | {
53 | return filter_var($wsdl, FILTER_VALIDATE_URL) !== false
54 | && in_array(parse_url($wsdl, PHP_URL_SCHEME), ['http', 'https']);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/functional/SoapClientTest.php:
--------------------------------------------------------------------------------
1 | factory = new Factory();
14 | }
15 |
16 | /**
17 | * @test
18 | */
19 | public function call()
20 | {
21 | $client = $this->factory->create(
22 | new Client(),
23 | 'http://www.webservicex.net/Statistics.asmx?WSDL'
24 | );
25 | $response = $client->call('GetStatistics', [['X' => [1,2,3]]]);
26 | $this->assertNotEmpty($response);
27 | }
28 |
29 | /**
30 | * @test
31 | * @dataProvider webServicesProvider
32 | */
33 | public function callAsync($wsdl, $options, $function, $args, $contains)
34 | {
35 | $client = $this->factory->create(
36 | new Client(),
37 | $wsdl,
38 | $options
39 | );
40 | $response = $client->callAsync($function, $args)->wait();
41 | $this->assertNotEmpty($response);
42 | foreach ($contains as $contain) {
43 | $this->assertArrayHasKey($contain, (array)$response);
44 | }
45 | }
46 |
47 | public function webServicesProvider()
48 | {
49 | return [
50 | [
51 | 'wsdl' => 'http://www.webservicex.net/Statistics.asmx?WSDL',
52 | 'options' => [],
53 | 'function' => 'GetStatistics',
54 | 'args' => [['X' => [1,2,3]]],
55 | 'contains' => [
56 | 'Sums', 'Average', 'StandardDeviation', 'skewness', 'Kurtosis'
57 | ]
58 | ],
59 | [
60 | 'wsdl' => 'http://www.webservicex.net/Statistics.asmx?WSDL',
61 | 'options' => ['soap_version' => SOAP_1_2],
62 | 'function' => 'GetStatistics',
63 | 'args' => [['X' => [1,2,3]]],
64 | 'contains' => [
65 | 'Sums', 'Average', 'StandardDeviation', 'skewness', 'Kurtosis'
66 | ]
67 | ],
68 | [
69 | 'wsdl' => 'http://www.webservicex.net/CurrencyConvertor.asmx?WSDL',
70 | 'options' => [],
71 | 'function' => 'ConversionRate',
72 | 'args' => [['FromCurrency' => 'GBP', 'ToCurrency' => 'USD']],
73 | 'contains' => [
74 | 'ConversionRateResult'
75 | ]
76 | ],
77 | [
78 | 'wsdl' => 'http://www.webservicex.net/CurrencyConvertor.asmx?WSDL',
79 | 'options' => ['soap_version' => SOAP_1_2],
80 | 'function' => 'ConversionRate',
81 | 'args' => [['FromCurrency' => 'GBP', 'ToCurrency' => 'USD']],
82 | 'contains' => [
83 | 'ConversionRateResult'
84 | ]
85 | ],
86 | [
87 | 'wsdl' => 'http://www.webservicex.net/bep.asmx?WSDL',
88 | 'options' => ['soap_version' => SOAP_1_1],
89 | 'function' => 'BreakEvenPoint',
90 | 'args' => [['FixedCost' => 1.1, 'VariableCost' => 1.2, 'ReturnsPerUnit' => 1.3]],
91 | 'contains' => [
92 | 'BreakEvenPointResult'
93 | ]
94 | ],
95 | [
96 | 'wsdl' => 'http://www.webservicex.net/bep.asmx?WSDL',
97 | 'options' => ['soap_version' => SOAP_1_2],
98 | 'function' => 'BreakEvenPoint',
99 | 'args' => [['FixedCost' => 1.1, 'VariableCost' => 1.2, 'ReturnsPerUnit' => 1.3]],
100 | 'contains' => [
101 | 'BreakEvenPointResult'
102 | ]
103 | ],
104 | ];
105 | }
106 | }
--------------------------------------------------------------------------------
/tests/unit/SoapClientTest.php:
--------------------------------------------------------------------------------
1 | handlerMock = new MockHandler();
36 | $handler = new HandlerStack($this->handlerMock);
37 | $this->client = new Client(['handler' => $handler]);
38 |
39 | $this->httpBindingMock = $this->getMockBuilder(HttpBinding::class)
40 | ->disableOriginalConstructor()
41 | ->setMethods(['request', 'response'])
42 | ->getMock();
43 | }
44 |
45 | /**
46 | * @test
47 | */
48 | public function magicCallDeferredHttpBindingRejected()
49 | {
50 | $this->httpBindingPromise = new RejectedPromise(new \Exception());
51 | $this->httpBindingMock->expects($this->never())->method('request');
52 |
53 | $client = new SoapClient($this->client, $this->httpBindingPromise);
54 | $this->expectException(\Exception::class);
55 | $client->someSoapMethod(['some-key' => 'some-value'])->wait();
56 | }
57 |
58 | /**
59 | * @test
60 | */
61 | public function magicCallHttpBindingFailed()
62 | {
63 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
64 |
65 | $this->httpBindingMock->method('request')
66 | ->will(
67 | $this->throwException(new RequestException())
68 | )
69 | ->with(
70 | 'someSoapMethod', [['some-key' => 'some-value']]
71 | );
72 |
73 | $this->httpBindingMock->expects($this->never())->method('response');
74 |
75 | $client = new SoapClient($this->client, $this->httpBindingPromise);
76 | $this->expectException(RequestException::class);
77 | $client->someSoapMethod(['some-key' => 'some-value'])->wait();
78 | }
79 |
80 | /**
81 | * @test
82 | */
83 | public function magicCall500Response()
84 | {
85 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
86 |
87 | $this->httpBindingMock->method('request')
88 | ->willReturn(
89 | new Request('POST', 'www.endpoint.com')
90 | )
91 | ->with(
92 | 'someSoapMethod', [['some-key' => 'some-value']]
93 | );
94 |
95 | $response = new Response('500');
96 | $this->httpBindingMock->method('response')
97 | ->willReturn(
98 | 'SoapResult'
99 | )
100 | ->with(
101 | $response, 'someSoapMethod', null
102 | );
103 |
104 | $this->handlerMock->append(GuzzleRequestException::create(new Request('POST', 'www.endpoint.com'), $response));
105 |
106 | $client = new SoapClient($this->client, $this->httpBindingPromise);
107 | $this->assertEquals('SoapResult', $client->someSoapMethod(['some-key' => 'some-value'])->wait());
108 | }
109 |
110 | /**
111 | * @test
112 | */
113 | public function magicCallResponseNotReceived()
114 | {
115 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
116 |
117 | $this->httpBindingMock->method('request')
118 | ->willReturn(
119 | new Request('POST', 'www.endpoint.com')
120 | )
121 | ->with(
122 | 'someSoapMethod', [['some-key' => 'some-value']]
123 | );
124 |
125 | $this->httpBindingMock->expects($this->never())->method('response');
126 |
127 | $this->handlerMock->append(GuzzleRequestException::create(new Request('POST', 'www.endpoint.com')));
128 |
129 | $client = new SoapClient($this->client, $this->httpBindingPromise);
130 | $this->expectException(GuzzleRequestException::class);
131 | $client->someSoapMethod(['some-key' => 'some-value'])->wait();
132 | }
133 |
134 | /**
135 | * @test
136 | */
137 | public function magicCallUndefinedResponse()
138 | {
139 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
140 |
141 | $this->httpBindingMock->method('request')
142 | ->willReturn(
143 | new Request('POST', 'www.endpoint.com')
144 | )
145 | ->with(
146 | 'someSoapMethod', [['some-key' => 'some-value']]
147 | );
148 |
149 | $this->httpBindingMock->expects($this->never())->method('response');
150 |
151 | $this->handlerMock->append(new \Exception());
152 |
153 | $client = new SoapClient($this->client, $this->httpBindingPromise);
154 | $this->expectException(\Exception::class);
155 | $client->someSoapMethod(['some-key' => 'some-value'])->wait();
156 |
157 | }
158 |
159 | /**
160 | * @test
161 | */
162 | public function magicCallClientReturnSoapFault()
163 | {
164 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
165 |
166 | $this->httpBindingMock->method('request')
167 | ->willReturn(
168 | new Request('POST', 'www.endpoint.com')
169 | )
170 | ->with(
171 | 'someSoapMethod', [['some-key' => 'some-value']]
172 | );
173 |
174 | $response = new Response('200', [], 'body');
175 | $this->httpBindingMock->method('response')
176 | ->will(
177 | $this->throwException(new \SoapFault('soap fault', 'soap fault'))
178 | )
179 | ->with(
180 | $response, 'someSoapMethod', null
181 | );
182 |
183 | $this->handlerMock->append($response);
184 |
185 | $client = new SoapClient($this->client, $this->httpBindingPromise);
186 | $this->expectException(\SoapFault::class);
187 | $client->someSoapMethod(['some-key' => 'some-value'])->wait();
188 | }
189 |
190 | /**
191 | * @test
192 | */
193 | public function magicCallSuccess()
194 | {
195 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
196 |
197 | $this->httpBindingMock->method('request')
198 | ->willReturn(
199 | new Request('POST', 'www.endpoint.com')
200 | )
201 | ->with(
202 | 'someSoapMethod', [['some-key' => 'some-value']]
203 | );
204 |
205 | $response = new Response('200', [], 'body');
206 | $this->httpBindingMock->method('response')
207 | ->willReturn(
208 | 'SoapResult'
209 | )
210 | ->with(
211 | $response, 'someSoapMethod', null
212 | );
213 |
214 | $this->handlerMock->append($response);
215 |
216 | $client = new SoapClient($this->client, $this->httpBindingPromise);
217 | $this->assertEquals('SoapResult', $client->someSoapMethod(['some-key' => 'some-value'])->wait());
218 | }
219 |
220 | /**
221 | * @test
222 | */
223 | public function resultsAreEquivalent()
224 | {
225 | $this->httpBindingPromise = new FulfilledPromise($this->httpBindingMock);
226 |
227 | $this->httpBindingMock->method('request')
228 | ->willReturn(
229 | new Request('POST', 'www.endpoint.com')
230 | )
231 | ->with(
232 | 'someSoapMethod', [['some-key' => 'some-value']]
233 | );
234 |
235 | $response = new Response('200', [], 'body');
236 | $this->httpBindingMock->method('response')->willReturn(
237 | 'SoapResult'
238 | );
239 |
240 | $this->handlerMock->append($response);
241 | $this->handlerMock->append($response);
242 | $this->handlerMock->append($response);
243 |
244 | $client = new SoapClient($this->client, $this->httpBindingPromise);
245 | $magicResult = $client->someSoapMethod(['some-key' => 'some-value'])->wait();
246 | $syncResult = $client->call('someSoapMethod', [['some-key' => 'some-value']]);
247 | $asyncResult = $client->callAsync('someSoapMethod', [['some-key' => 'some-value']])->wait();
248 | $this->assertEquals($magicResult, $asyncResult);
249 | $this->assertEquals($syncResult, $asyncResult);
250 | }
251 | }
252 |
--------------------------------------------------------------------------------