├── .php-cs-fixer.dist.php
├── LICENSE
├── README.md
├── composer.json
├── examples
└── psr18-transport.php
├── psalm.xml
└── src
├── Exception
└── RequestException.php
├── HttpBinding
├── Psr7Converter.php
├── Psr7RequestBuilder.php
└── SoapActionDetector.php
├── Middleware
├── RemoveEmptyNodesMiddleware.php
├── SoapHeaderMiddleware.php
├── WSICompliance
│ └── QuotedSoapActionMiddleware.php
└── Wsdl
│ ├── DisableExtensionsMiddleware.php
│ └── DisablePoliciesMiddleware.php
├── Psr18Transport.php
├── Wsdl
└── Psr18Loader.php
└── Xml
├── Loader
└── Psr7StreamLoader.php
├── Mapper
└── Psr7StreamMapper.php
└── XmlMessageManipulator.php
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | setFinder(
5 | \Symfony\Component\Finder\Finder::create()
6 | ->in([
7 | __DIR__ . '/src',
8 | __DIR__ . '/tests',
9 | ])
10 | ->name('*.php')
11 | )
12 | ->setRiskyAllowed(true)
13 | ->setRules([
14 | '@PSR2' => true,
15 | 'align_multiline_comment' => true,
16 | 'array_indentation' => true,
17 | 'declare_strict_types' => true,
18 | 'final_class' => true,
19 | 'global_namespace_import' => [
20 | 'import_classes' => true,
21 | 'import_constants' => true,
22 | 'import_functions' => true,
23 | ],
24 | 'list_syntax' => [
25 | 'syntax' => 'short',
26 | ],
27 | 'constant_case' => [
28 | 'case' => 'lower',
29 | ],
30 | 'multiline_comment_opening_closing' => true,
31 | 'native_function_casing' => true,
32 | 'no_empty_phpdoc' => true,
33 | 'no_leading_import_slash' => true,
34 | 'no_superfluous_phpdoc_tags' => [
35 | 'allow_mixed' => true,
36 | ],
37 | 'no_unused_imports' => true,
38 | 'no_useless_else' => true,
39 | 'no_useless_return' => true,
40 | 'ordered_imports' => [
41 | 'imports_order' => ['class', 'function', 'const'],
42 | ],
43 | 'ordered_interfaces' => true,
44 | 'php_unit_test_annotation' => true,
45 | 'php_unit_test_case_static_method_calls' => [
46 | 'call_type' => 'static',
47 | ],
48 | 'php_unit_method_casing' => [
49 | 'case' => 'snake_case',
50 | ],
51 | 'single_import_per_statement' => true,
52 | 'single_trait_insert_per_statement' => true,
53 | 'static_lambda' => true,
54 | 'strict_comparison' => true,
55 | 'strict_param' => true,
56 | 'nullable_type_declaration_for_default_null_value' => true,
57 | ])
58 | ;
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021-MAX_INT php-soap
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
13 | all 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 | # PSR-18 SOAP Transport
2 |
3 | This transport allows you to send SOAP requests over a PSR-18 HTTP client implementation.
4 | You can use any client you want, going from curl, guzzle, httplug, symfony/http-client, ...
5 | It allows you to get full control over the HTTP layer, making it possible to e.g. overcome some well-known issues in `ext-soap`.
6 | This package can best be used together with a [SOAP driver](https://github.com/php-soap/engine) that handles data encoding and decoding.
7 |
8 | # Want to help out? 💚
9 |
10 | - [Become a Sponsor](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#sponsor)
11 | - [Let us do your implementation](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#let-us-do-your-implementation)
12 | - [Contribute](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#contribute)
13 | - [Help maintain these packages](https://github.com/php-soap/.github/blob/main/HELPING_OUT.md#maintain)
14 |
15 | Want more information about the future of this project? Check out this list of the [next big projects](https://github.com/php-soap/.github/blob/main/PROJECTS.md) we'll be working on.
16 |
17 | # Prerequisites
18 |
19 | Choosing what HTTP client you want to use.
20 | This package expects some PSR implementations to be present in order to be installed:
21 |
22 | * PSR-7: `psr/http-message-implementation` like `nyholm/psr7` or `guzzlehttp/psr7`
23 | * PSR-17: `psr/http-factory-implementation` like `nyholm/psr7` or `guzzlehttp/psr7`
24 | * PSR-18: `psr/http-client-implementation` like `symfony/http-client` or `guzzlehttp/guzzle`
25 |
26 | # Installation
27 |
28 | ```bash
29 | composer require php-soap/psr18-transport
30 | ```
31 |
32 | ## Usage
33 |
34 | ```php
35 |
36 | use Http\Client\Common\PluginClient;
37 | use Soap\Engine\SimpleEngine;
38 | use Soap\Psr18Transport\Psr18Transport;
39 |
40 | $engine = new SimpleEngine(
41 | $driver,
42 | $transport = Psr18Transport::createForClient(
43 | new PluginClient(
44 | $psr18Client,
45 | [...$middleware]
46 | )
47 | )
48 | );
49 | ```
50 |
51 | ## Middleware
52 |
53 | This package provides some middleware implementations for dealing with some common SOAP issues.
54 |
55 | ### Wsdl\DisableExtensionsMiddleware
56 |
57 | PHP's ext-soap implementation do not support `wsdl:required` attributes since there is no SOAP extension mechanism in PHP.
58 | You will retrieve this exception: "[SoapFault] SOAP-ERROR: Parsing WSDL: Unknown required WSDL extension"
59 | when the WSDL does contain required SOAP extensions.
60 |
61 | This middleware can be used to set the "wsdl:required"
62 | property to false when loading the WSDL so that you don't have to change the WSDL on the server.
63 |
64 | **Usage**
65 |
66 | ```php
67 | use Http\Client\Common\PluginClient;
68 | use Soap\Psr18Transport\Middleware\Wsdl\DisableExtensionsMiddleware;
69 |
70 | $wsdlClient = new PluginClient(
71 | $psr18Client,
72 | [
73 | new DisableExtensionsMiddleware(),
74 | ]
75 | );
76 | ```
77 |
78 | ### Wsdl\DisablePoliciesMiddleware
79 |
80 | PHP's ext-soap client does not support the [Web Services Policy Framework](http://schemas.xmlsoap.org/ws/2004/09/policy/) attributes since there is no such support in PHP.
81 | You will retrieve this exception: "[SoapFault] SOAP-ERROR: Parsing WSDL: Unknown required WSDL extension 'http://schemas.xmlsoap.org/ws/2004/09/policy'"
82 | when the WSDL does contains WS policies.
83 |
84 | This middleware can be used to remove all UsingPolicy and Policy tags on the fly so that you don't have to change the WSDL on the server.
85 |
86 | **Usage**
87 |
88 | ```php
89 | use Http\Client\Common\PluginClient;
90 | use Soap\Psr18Transport\Middleware\Wsdl\DisablePoliciesMiddleware;
91 |
92 | $wsdlClient = new PluginClient(
93 | $psr18Client,
94 | [
95 | new DisablePoliciesMiddleware(),
96 | ]
97 | );
98 | ```
99 |
100 | ### RemoveEmptyNodesMiddleware
101 |
102 | Empty properties are converted into empty nodes in the request XML.
103 | If you need to avoid empty nodes in the request xml, you can add this middleware.
104 |
105 | **Usage**
106 |
107 | ```php
108 | use Http\Client\Common\PluginClient;
109 | use Soap\Psr18Transport\Middleware\RemoveEmptyNodesMiddleware;
110 |
111 |
112 | $httpClient = new PluginClient(
113 | $psr18Client,
114 | [
115 | new RemoveEmptyNodesMiddleware()
116 | ]
117 | );
118 | ```
119 |
120 | ### SoapHeaderMiddleware
121 |
122 | Attaches multiple SOAP headers to the request before sending the SOAP envelope.
123 |
124 | **Usage**
125 |
126 | ```php
127 | use Http\Client\Common\PluginClient;
128 | use Soap\Psr18Transport\Middleware\RemoveEmptyNodesMiddleware;
129 | use Soap\Xml\Builder\Header\Actor;
130 | use Soap\Xml\Builder\Header\MustUnderstand;
131 | use Soap\Xml\Builder\SoapHeader;
132 |
133 |
134 | $httpClient = new PluginClient(
135 | $psr18Client,
136 | [
137 | new SoapHeaderMiddleware(
138 | new SoapHeader(
139 | $tns,
140 | 'x:Auth',
141 | children(
142 | namespaced_element($tns, 'x:user', value('josbos')),
143 | namespaced_element($tns, 'x:password', value('topsecret'))
144 | )
145 | ),
146 | new SoapHeader($tns, 'Acting', Actor::next()),
147 | new SoapHeader($tns, 'Understanding', new MustUnderstand())
148 | )
149 | ]
150 | );
151 | ```
152 |
153 | More information on the SoapHeader configurator can be found in [php-soap/xml](https://github.com/php-soap/xml#soapheaders).
154 |
155 | ### HTTPlug middleware
156 |
157 | This package includes [all basic plugins from httplug](https://docs.php-http.org/en/latest/plugins/).
158 | You can load any additional plugins you want, like e.g. [the logger plugin](https://github.com/php-http/logger-plugin).
159 |
160 | **Examples**
161 |
162 | ```php
163 | use Http\Client\Common\Plugin\AuthenticationPlugin;
164 | use Http\Client\Common\Plugin\BaseUriPlugin;
165 | use Http\Client\Common\Plugin\LoggerPlugin;
166 | use Http\Message\Authentication\BasicAuth;
167 |
168 |
169 | $httpClient = new PluginClient(
170 | $psr18Client,
171 | [
172 | new BaseUriPlugin($baseLocation),
173 | new AuthenticationPlugin(new BasicAuth($_ENV['user'], $_ENV['pass'])),
174 | new LoggerPlugin($psrLogger),
175 | ]
176 | );
177 | ```
178 |
179 | ### Writing your own middleware
180 |
181 | We use httplug for its plugin system.
182 | You can create your own middleware by [following their documentation](https://docs.php-http.org/en/latest/plugins/build-your-own.html).
183 |
184 | ## Authentication
185 |
186 | You can add authentication to both the WSDL fetching and SOAP handling part.
187 | For this, we suggest you to use the default [httplug authentication providers](https://docs.php-http.org/en/latest/message/authentication.html).
188 |
189 | ### NTLM
190 |
191 | Adding NTLM authentication requires you to use a `curl` based PSR-18 HTTP Client.
192 | On those clients, you can set following options: `[CURLOPT_HTTPAUTH => CURLAUTH_NTLM, CURLOPT_USERPWD => 'user:pass']`.
193 | Clients like guzzle and symfony/http-client also support NTLM by setting options during client configuration.
194 |
195 | ## Dealing with XML
196 |
197 | When writing custom SOAP middleware, a frequent task is to transform the request or response XML into a slight variation.
198 | This package provides some shortcut tools around [php-soap/xml](https://github.com/php-soap/xml) to make it easier for you to deal with XML.
199 |
200 |
201 | **Example**
202 |
203 | ```php
204 | use Http\Client\Common\Plugin;
205 | use Http\Promise\Promise;
206 | use Psr\Http\Message\RequestInterface;
207 | use Psr\Http\Message\ResponseInterface;
208 | use Soap\Psr18Transport\Xml\XmlMessageManipulator;
209 | use VeeWee\Xml\Dom\Document;
210 |
211 | class SomeMiddleware implements Plugin
212 | {
213 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
214 | {
215 | $request = (new XmlMessageManipulator)(
216 | $request,
217 | fn (Document $document) => $document->manipulate(
218 | doSomethingWithRequestXml()
219 | )
220 | );
221 |
222 | return $next($request)
223 | ->then(function (ResponseInterface $response): ResponseInterface {
224 | return (new XmlMessageManipulator)(
225 | $response,
226 | fn (Document $document) => $document->manipulate(
227 | doSomethingWithResponseXml()
228 | )
229 | );
230 | });
231 | }
232 | }
233 | ```
234 |
235 | ## Loading WSDL with PSR-18 clients
236 |
237 | For loading WSDL's, you might want to use a PSR-18 client to do the hard HTTP work.
238 | This allows you for advances setups in which the WSDL is put behind basic authentication.
239 | This package provides a PSR-18 [WSDL loader](https://github.com/php-soap/wsdl#wsdl-loader) that can be used to load HTTP locations with your favourite HTTP client.
240 | It can be used in combinations with for example the WSDL loaders from the [php-soap/ext-soap-engine](https://github.com/php-soap/ext-soap-engine).
241 |
242 | ### Psr18Loader
243 |
244 | **Examples**
245 |
246 | ```php
247 | use Http\Client\Common\PluginClient;
248 | use Soap\Psr18Transport\Wsdl\Psr18Loader;
249 | use Soap\Wsdl\Loader\FlatteningLoader;
250 |
251 | $loader = Psr18Loader::createForClient(
252 | $wsdlClient = new PluginClient(
253 | $psr18Client,
254 | [...$middleware]
255 | )
256 | );
257 |
258 | // If you want to flatten all imports whilst using this PSR-18 loader:
259 | $loader = new FlatteningLoader($loader);
260 |
261 |
262 | $payload = $loader('http://some.wsdl');
263 | ```
264 |
265 | *NOTE:* If you want to flatten the imports inside the WSDL, you'll have to combine this loader with the the [FlatteningLoader](https://github.com/php-soap/wsdl#flatteningloader).
266 |
267 |
268 | ## Async SOAP calls
269 |
270 | Since PHP 8.1, fibers are introduced to PHP.
271 | This means that you can use any fiber based PSR-18 client in order to send async calls.
272 |
273 | Here is a short example for `react/http` in combination with `react/async`.
274 |
275 | ```sh
276 | composer require react/async veewee/psr18-react-browser
277 | ```
278 |
279 | *(There currently is no official fiber based PSR-18 implementation of either AMP or ReactPHP. Therefore, [a small bridge can be used intermediately](https://github.com/veewee/psr18-react-browser))*
280 |
281 |
282 | Usage:
283 |
284 | ```php
285 | use Http\Client\Common\PluginClient;
286 | use Soap\Engine\SimpleEngine;
287 | use Soap\ExtSoapEngine\ExtSoapDriver;
288 | use Soap\ExtSoapEngine\ExtSoapOptions;
289 | use Soap\ExtSoapEngine\Wsdl\TemporaryWsdlLoaderProvider;
290 | use Soap\Psr18Transport\Psr18Transport;
291 | use Soap\Psr18Transport\Wsdl\Psr18Loader;
292 | use Soap\Wsdl\Loader\FlatteningLoader;
293 | use Veewee\Psr18ReactBrowser\Psr18ReactBrowserClient;
294 | use function React\Async\async;
295 | use function React\Async\await;
296 | use function React\Async\parallel;
297 |
298 | $asyncHttpClient = Psr18ReactBrowserClient::default();
299 | $engine = new SimpleEngine(
300 | ExtSoapDriver::createFromClient(
301 | $client = AbusedClient::createFromOptions(
302 | ExtSoapOptions::defaults('http://www.dneonline.com/calculator.asmx?wsdl', [])
303 | ->disableWsdlCache()
304 | )
305 | ),
306 | $transport = Psr18Transport::createForClient(
307 | new PluginClient(
308 | $asyncHttpClient,
309 | [...$middleware]
310 | )
311 | )
312 | );
313 |
314 | $add = async(fn ($a, $b) => $engine->request('Add', [['intA' => $a, 'intB' => $b]]));
315 | $addWithLogger = fn ($a, $b) => $add($a, $b)->then(
316 | function ($result) use ($a, $b) {
317 | echo "SUCCESS {$a}+{$b} = ${result}!" . PHP_EOL;
318 | return $result;
319 | },
320 | function (Exception $e) {
321 | echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
322 | }
323 | );
324 |
325 | $results = await(parallel([
326 | fn() => $addWithLogger(1, 2),
327 | fn() => $addWithLogger(3, 4),
328 | fn() => $addWithLogger(5, 6)
329 | ]));
330 |
331 | var_dump($results);
332 | ```
333 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "php-soap/psr18-transport",
3 | "description": "PSR-18 HTTP Client transport for SOAP",
4 | "type": "library",
5 | "license": "MIT",
6 | "autoload": {
7 | "psr-4": {
8 | "Soap\\Psr18Transport\\": "src/"
9 | }
10 | },
11 | "autoload-dev": {
12 | "psr-4": {
13 | "SoapTest\\Psr18Transport\\": "tests/"
14 | }
15 | },
16 | "authors": [
17 | {
18 | "name": "Toon Verwerft",
19 | "email": "toonverwerft@gmail.com"
20 | }
21 | ],
22 | "require": {
23 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0",
24 | "ext-dom": "*",
25 | "php-soap/engine": "^2.13",
26 | "php-soap/wsdl": "^1.12",
27 | "php-soap/xml": "^1.8",
28 | "php-http/discovery": "^1.12",
29 | "psr/http-client-implementation": "^1.0",
30 | "psr/http-factory-implementation": "^1.0",
31 | "psr/http-message-implementation": "^1.0",
32 | "psr/http-message": "^1.0.1|^2.0",
33 | "veewee/xml": "^3.0",
34 | "php-http/client-common": "^2.3"
35 | },
36 | "require-dev": {
37 | "ext-soap": "*",
38 | "nyholm/psr7": "^1.5",
39 | "php-http/mock-client": "^1.5",
40 | "php-soap/ext-soap-engine": "^1.7",
41 | "php-soap/engine-integration-tests": "^1.9",
42 | "phpunit/phpunit": "^10.0 || ^11.0",
43 | "guzzlehttp/guzzle": "^7.5"
44 | },
45 | "config": {
46 | "allow-plugins": {
47 | "php-http/discovery": true
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/psr18-transport.php:
--------------------------------------------------------------------------------
1 | disableWsdlCache()
17 | )
18 | ),
19 | $transport = new TraceableTransport(
20 | $client,
21 | Psr18Transport::createForClient(new \GuzzleHttp\Client())
22 | )
23 | );
24 |
25 | $result = $engine->request('Add', [['intA' => 1, 'intB' => 2]]);
26 |
27 | var_dump($result);
28 | var_dump($transport->collectLastRequestInfo());
29 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Exception/RequestException.php:
--------------------------------------------------------------------------------
1 | getMessage(), (int) $exception->getCode(), $exception);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/HttpBinding/Psr7Converter.php:
--------------------------------------------------------------------------------
1 | requestFactory = $requestFactory;
22 | $this->streamFactory = $streamFactory;
23 | }
24 |
25 | public function convertSoapRequest(SoapRequest $request): RequestInterface
26 | {
27 | $builder = new Psr7RequestBuilder($this->requestFactory, $this->streamFactory);
28 |
29 | $request->isSOAP11() ? $builder->isSOAP11() : $builder->isSOAP12();
30 | $builder->setEndpoint($request->getLocation());
31 | $builder->setSoapAction($request->getAction());
32 | $builder->setSoapMessage($request->getRequest());
33 |
34 | return $builder->getHttpRequest();
35 | }
36 |
37 | public function convertSoapResponse(ResponseInterface $response): SoapResponse
38 | {
39 | return new SoapResponse(
40 | (string) $response->getBody()
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/HttpBinding/Psr7RequestBuilder.php:
--------------------------------------------------------------------------------
1 | requestFactory = $requestFactory;
38 | $this->streamFactory = $streamFactory;
39 | }
40 |
41 | /**
42 | * @throws RequestException
43 | */
44 | public function getHttpRequest(): RequestInterface
45 | {
46 | $this->validate();
47 |
48 | try {
49 | $request = $this->requestFactory
50 | ->createRequest($this->httpMethod, $this->endpoint)
51 | ->withBody($this->prepareMessage());
52 |
53 | foreach ($this->prepareHeaders() as $name => $value) {
54 | $request = $request->withHeader($name, $value);
55 | }
56 | } catch (InvalidArgumentException $e) {
57 | throw RequestException::fromException($e);
58 | }
59 |
60 | return $request;
61 | }
62 |
63 | public function setEndpoint(string $endpoint): void
64 | {
65 | $this->endpoint = $endpoint;
66 | }
67 |
68 | /**
69 | * Mark as SOAP 1.1
70 | */
71 | public function isSOAP11(): void
72 | {
73 | $this->soapVersion = self::SOAP11;
74 | }
75 |
76 | /**
77 | * Mark as SOAP 1.2
78 | */
79 | public function isSOAP12(): void
80 | {
81 | $this->soapVersion = self::SOAP12;
82 | }
83 |
84 |
85 |
86 | public function setSoapAction(string $soapAction): void
87 | {
88 | $this->soapAction = $soapAction;
89 | }
90 |
91 | public function setSoapMessage(string $content): void
92 | {
93 | $this->soapMessage = $this->streamFactory->createStream($content);
94 | $this->hasSoapMessage = true;
95 | }
96 |
97 | public function setHttpMethod(string $method): void
98 | {
99 | $this->httpMethod = $method;
100 | }
101 |
102 | /**
103 | * @throws RequestException
104 | */
105 | private function validate(): void
106 | {
107 | if (!$this->endpoint) {
108 | throw RequestException::noEndpoint();
109 | }
110 |
111 | if (!$this->hasSoapMessage && $this->httpMethod === 'POST') {
112 | throw RequestException::noMessage();
113 | }
114 |
115 | /**
116 | * SOAP 1.1 only defines HTTP binding with POST method.
117 | * @link https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383527
118 | */
119 | if ($this->soapVersion === self::SOAP11 && $this->httpMethod !== 'POST') {
120 | throw RequestException::postNotAllowedForSoap11();
121 | }
122 |
123 | /**
124 | * SOAP 1.2 only defines HTTP binding with POST and GET methods.
125 | * @link https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L10309
126 | */
127 | if ($this->soapVersion === self::SOAP12 && !in_array($this->httpMethod, ['GET', 'POST'], true)) {
128 | throw RequestException::invalidMethodForSoap12();
129 | }
130 | }
131 |
132 | /**
133 | * @return array
134 | */
135 | private function prepareHeaders(): array
136 | {
137 | if ($this->soapVersion === self::SOAP11) {
138 | return $this->prepareSoap11Headers();
139 | }
140 |
141 | return $this->prepareSoap12Headers();
142 | }
143 |
144 | /**
145 | * @link https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383526
146 | * @return array
147 | */
148 | private function prepareSoap11Headers(): array
149 | {
150 | $headers = [];
151 | $headers['SOAPAction'] = $this->prepareQuotedSoapAction($this->soapAction);
152 | $headers['Content-Type'] = 'text/xml; charset="utf-8"';
153 |
154 | return array_filter($headers);
155 | }
156 |
157 | /**
158 | * SOAPAction header is removed in SOAP 1.2 and now expressed as a value of
159 | * an (optional) "action" parameter of the "application/soap+xml" media type.
160 | * @link https://www.w3.org/TR/soap12-part0/#L4697
161 | * @return array
162 | */
163 | private function prepareSoap12Headers(): array
164 | {
165 | $headers = [];
166 | if ($this->httpMethod !== 'POST') {
167 | $headers['Accept'] = 'application/soap+xml';
168 | return $headers;
169 | }
170 |
171 | $soapAction = $this->prepareQuotedSoapAction($this->soapAction);
172 | $headers['Content-Type'] = 'application/soap+xml; charset="utf-8"' . '; action='.$soapAction;
173 |
174 | return array_filter($headers);
175 | }
176 |
177 | private function prepareMessage(): StreamInterface
178 | {
179 | if ($this->httpMethod === 'POST') {
180 | return $this->soapMessage ?? $this->streamFactory->createStream('');
181 | }
182 |
183 | return $this->streamFactory->createStream('');
184 | }
185 |
186 | private function prepareQuotedSoapAction(string $soapAction): string
187 | {
188 | $soapAction = trim($soapAction, '"\'');
189 |
190 | return '"'.$soapAction.'"';
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/HttpBinding/SoapActionDetector.php:
--------------------------------------------------------------------------------
1 | trim($action, '"\'');
15 | $header = $request->getHeader('SOAPAction');
16 | if ($header) {
17 | return $normalize($header[0]);
18 | }
19 |
20 | $contentTypes = $request->getHeader('Content-Type');
21 | if ($contentTypes) {
22 | $contentType = $contentTypes[0];
23 | foreach (explode(';', $contentType) as $part) {
24 | if (strpos($part, 'action=') !== false) {
25 | return $normalize(explode('=', $part)[1]);
26 | }
27 | }
28 | }
29 |
30 | throw new RequestException('SOAP Action not found in HTTP headers.', $request);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Middleware/RemoveEmptyNodesMiddleware.php:
--------------------------------------------------------------------------------
1 | xpath(new EnvelopePreset($xml));
22 |
23 | do {
24 | $emptyNodes = $xpath->query('//soap:Envelope/*//*[not(node())]');
25 | $emptyNodes->forEach(
26 | static fn (DOMNode $element) => remove($element)
27 | );
28 | } while ($emptyNodes->count());
29 | }
30 | );
31 |
32 | return $next($request);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Middleware/SoapHeaderMiddleware.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | private array $configurators;
22 |
23 | /**
24 | * @no-named-arguments
25 | * @param list $configurators
26 | */
27 | public function __construct(callable ... $configurators)
28 | {
29 | $this->configurators = $configurators;
30 | }
31 |
32 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
33 | {
34 | return $next((new XmlMessageManipulator)(
35 | $request,
36 | function (Document $document) {
37 | /** @var list $headers */
38 | $headers = $document->build(new SoapHeaders(...$this->configurators));
39 |
40 | return $document->manipulate(new PrependSoapHeaders(...$headers));
41 | }
42 | ));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Middleware/WSICompliance/QuotedSoapActionMiddleware.php:
--------------------------------------------------------------------------------
1 | withHeader('SOAPAction', '"'.$soapAction.'"'));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Middleware/Wsdl/DisableExtensionsMiddleware.php:
--------------------------------------------------------------------------------
1 | then(function (ResponseInterface $response): ResponseInterface {
22 | return (new XmlMessageManipulator)(
23 | $response,
24 | fn (Document $document) => $this->disableExtensions($document)
25 | );
26 | });
27 | }
28 |
29 | private function disableExtensions(Document $document): void
30 | {
31 | $namespace = $document->locate(root_namespace_uri());
32 | $document->xpath(new WsdlPreset($document))
33 | ->query('//wsdl:binding//*[@wsdl:required]')
34 | ->expectAllOfType(DOMElement::class)
35 | ->forEach(
36 | static function (DOMElement $element) use ($namespace): void {
37 | namespaced_attribute($namespace ?? '', 'wsdl:required', 'false')($element);
38 | }
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Middleware/Wsdl/DisablePoliciesMiddleware.php:
--------------------------------------------------------------------------------
1 | then(function (ResponseInterface $response): ResponseInterface {
21 | return (new XmlMessageManipulator)(
22 | $response,
23 | fn (Document $document) => $this->disablePolicies($document)
24 | );
25 | });
26 | }
27 |
28 | public function disablePolicies(Document $document): void
29 | {
30 | $xpath = $document->xpath(
31 | namespaces([
32 | 'wsd' => 'http://schemas.xmlsoap.org/ws/2004/09/policy'
33 | ])
34 | );
35 |
36 | // remove all "UsingPolicy" tags
37 | $xpath->query('//wsd:UsingPolicy')
38 | ->expectAllOfType(DOMElement::class)
39 | ->forEach(
40 | static function (DOMElement $element): void {
41 | remove($element);
42 | }
43 | );
44 |
45 | // remove all "Policy" tags
46 | $xpath->query('//wsd:Policy')
47 | ->expectAllOfType(DOMElement::class)
48 | ->forEach(
49 | static function (DOMElement $element): void {
50 | remove($element);
51 | }
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Psr18Transport.php:
--------------------------------------------------------------------------------
1 | client = $client;
25 | $this->converter = $converter;
26 | }
27 |
28 | public static function createWithDefaultClient(): self
29 | {
30 | return self::createForClient(Psr18ClientDiscovery::find());
31 | }
32 |
33 | public static function createForClient(ClientInterface $client): self
34 | {
35 | return new self(
36 | $client,
37 | new Psr7Converter(
38 | Psr17FactoryDiscovery::findRequestFactory(),
39 | Psr17FactoryDiscovery::findStreamFactory()
40 | )
41 | );
42 | }
43 |
44 | public function request(SoapRequest $request): SoapResponse
45 | {
46 | $psr7Request = $this->converter->convertSoapRequest($request);
47 | $psr7Response = $this->client->sendRequest($psr7Request);
48 |
49 | return $this->converter->convertSoapResponse($psr7Response);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Wsdl/Psr18Loader.php:
--------------------------------------------------------------------------------
1 | client = $client;
20 | $this->requestFactory = $requestFactory;
21 | }
22 |
23 | public static function createForClient(ClientInterface $client): self
24 | {
25 | return new self(
26 | $client,
27 | Psr17FactoryDiscovery::findRequestFactory(),
28 | );
29 | }
30 |
31 | public function __invoke(string $location): string
32 | {
33 | $response = $this->client->sendRequest(
34 | $this->requestFactory->createRequest('GET', $location)
35 | );
36 |
37 | $body = $response->getBody();
38 | $body->rewind();
39 |
40 | return (string) $body;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Xml/Loader/Psr7StreamLoader.php:
--------------------------------------------------------------------------------
1 | stream = $stream;
20 | }
21 |
22 | /**
23 | * @throws RequestException
24 | */
25 | public function __invoke(DOMDocument $document): void
26 | {
27 | $this->stream->rewind();
28 | $contents = (string) $this->stream;
29 | if (!$contents) {
30 | throw RequestException::noMessage();
31 | }
32 |
33 | xml_string_loader($contents)($document);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Xml/Mapper/Psr7StreamMapper.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | final class Psr7StreamMapper implements Mapper
16 | {
17 | public function __invoke(DOMDocument $document): StreamInterface
18 | {
19 | $factory = Psr17FactoryDiscovery::findStreamFactory();
20 | $stream = $factory->createStream($document->saveXML());
21 | $stream->rewind();
22 |
23 | return $stream;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Xml/XmlMessageManipulator.php:
--------------------------------------------------------------------------------
1 | getBody())));
24 | $manipulator($document);
25 |
26 | return $message->withBody($document->map(new Psr7StreamMapper()));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------