Issues (6)

src/ParallelSoapClient.php (3 issues)

1
<?php
2
3
namespace Soap;
4
5
use Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerAwareTrait;
7
use Psr\Log\NullLogger;
8
9
/**
10
 * Single/Parallel Soap Class
11
 *
12
 * Implements soap with multi server-to-server calls using curl module.
13
 *
14
 * @author Mohamed Meabed <[email protected]>
15
 * @link   https://github.com/Meabed/php-parallel-soap
16
 * @note   Check the Example files and read the documentation carefully
17
 */
18
class ParallelSoapClient extends \SoapClient implements LoggerAwareInterface
19
{
20
    use LoggerAwareTrait;
21
    /** @var array of all responses in the client */
22
    public $soapResponses = [];
23
24
    /** @var array of all requests in the client */
25
    public $soapRequests = [];
26
27
    /** @var array of all requests xml in the client */
28
    public $requestXmlArr = [];
29
30
    /** @var string the xml returned from soap call */
31
    public $xmlResponse;
32
33
    /** @var string current method call */
34
    public $soapMethod;
35
36
    /** @var array of all requests soap methods in the client */
37
    public $soapMethodArr = [];
38
39
    /** @var array of all requestIds */
40
    public $requestIds;
41
42
    /** @var string last request id */
43
    public $lastRequestId;
44
45
    /** @var bool soap parallel flag */
46
    public $multi = false;
47
48
    /** @var bool log soap request */
49
    public $logSoapRequest = false;
50
51
    /** @var array defaultHeaders used for curl request */
52
    public $defaultHeaders = [
53
        'Content-type' => 'Content-type: text/xml;charset=UTF-8',
54
        'Accept' => 'Accept: text/xml',
55
        // Empty Expect @https://gms.tf/when-curl-sends-100-continue.html
56
        // @https://stackoverflow.com/questions/7551332/do-we-need-the-expect-100-continue-header-in-the-xfire-request-header
57
        'Expect' => 'Expect:',
58
    ];
59
60
    /** @var array of all curl_info in the client */
61
    public $curlInfo = [];
62
63
    /** @var array curl options */
64
    public $curlOptions = [];
65
66
    /** @var \Closure */
67
    protected $debugFn;
68
69
    /** @var \Closure */
70
    protected $formatXmlFn;
71
72
    /** @var \Closure */
73
    protected $resFn;
74
75
    /** @var \Closure */
76
    protected $soapActionFn;
77
78
    /** @var array curl share ssl */
79
    public $sharedCurlData = [];
80
81
    /** @var string getRequestResponse action constant used for parsing the xml with from parent::__doRequest */
82
    const GET_RESPONSE_CONST = 'getRequestResponseMethod';
83
    /** @var string text prefix, if error happen due to SOAP error before its executed ex:invalid method */
84
    const ERROR_STR = '*ERROR*';
85
86
    /**
87
     * @return mixed
88
     */
89
    public function getMulti()
90
    {
91
        return $this->multi;
92
    }
93
94
    /**
95
     * @param mixed $multi
96
     * @return ParallelSoapClient
97
     */
98 10
    public function setMulti($multi)
99
    {
100 10
        $this->multi = $multi;
101 10
        return $this;
102
    }
103
104
    /**
105
     * @return array
106
     */
107
    public function getCurlOptions()
108
    {
109
        return $this->curlOptions;
110
    }
111
112
    /**
113
     * @param array $curlOptions
114
     * @return ParallelSoapClient
115
     */
116
    public function setCurlOptions(array $curlOptions)
117
    {
118
        $this->curlOptions = $curlOptions;
119
        return $this;
120
    }
121
122
    /**
123
     * @return bool
124
     */
125
    public function isLogSoapRequest()
126
    {
127
        return $this->logSoapRequest;
128
    }
129
130
    /**
131
     * @param bool $logSoapRequest
132
     * @return ParallelSoapClient
133
     */
134
    public function setLogSoapRequest(bool $logSoapRequest)
135
    {
136
        $this->logSoapRequest = $logSoapRequest;
137
        return $this;
138
    }
139
140
    /**
141
     * @return \Closure
142
     */
143
    public function getDebugFn()
144
    {
145
        return $this->debugFn;
146
    }
147
148
    /**
149
     * @param \Closure $debugFn
150
     * @return ParallelSoapClient
151
     */
152
    public function setDebugFn(\Closure $debugFn)
153
    {
154
        $this->debugFn = $debugFn;
155
        return $this;
156
    }
157
158
    /**
159
     * @return \Closure
160
     */
161
    public function getFormatXmlFn()
162
    {
163
        return $this->formatXmlFn;
164
    }
165
166
    /**
167
     * @param \Closure $formatXmlFn
168
     * @return ParallelSoapClient
169
     */
170
    public function setFormatXmlFn(\Closure $formatXmlFn)
171
    {
172
        $this->formatXmlFn = $formatXmlFn;
173
        return $this;
174
    }
175
176
    /**
177
     * @return \Closure
178
     */
179
    public function getResFn()
180
    {
181
        return $this->resFn;
182
    }
183
184
    /**
185
     * @param \Closure $resFn
186
     * @return ParallelSoapClient
187
     */
188
    public function setResFn(\Closure $resFn)
189
    {
190
        $this->resFn = $resFn;
191
        return $this;
192
    }
193
194
    /**
195
     * @return \Closure
196
     */
197
    public function getSoapActionFn()
198
    {
199
        return $this->soapActionFn;
200
    }
201
202
    /**
203
     * @param \Closure $soapActionFn
204
     * @return ParallelSoapClient
205
     */
206
    public function setSoapActionFn(\Closure $soapActionFn)
207
    {
208
        $this->soapActionFn = $soapActionFn;
209
        return $this;
210
    }
211
212
213 10
    public function __construct($wsdl, array $options = null)
214
    {
215
        // logger
216
        $logger = $options['logger'] ?? new NullLogger();
217
        $this->setLogger($logger);
218
219
        // debug function to add headers / last request / response / etc...
220 10
        $debugFn = $options['debugFn'] ?? function ($res, $id) {
221 10
            };
222
        $this->setDebugFn($debugFn);
223
224
        // format xml before logging
225 2
        $formatXmlFn = $options['formatXmlFn'] ?? function ($xml) {
226 2
                return $xml;
227
            };
228
        $this->setFormatXmlFn($formatXmlFn);
229
230
        // result parsing function
231
        $resFn = $options['resFn'] ?? function ($method, $res) {
232
                return $res;
233
            };
234
        $this->setResFn($resFn);
235
236
        // soapAction function to set in the header
237
        // Ex: SOAPAction: "http://tempuri.org/SOAP.Demo.AddInteger"
238
        // Ex: SOAPAction: "http://webservices.amadeus.com/PNRRET_11_3_1A"
239 2
        $soapActionFn = $options['soapActionFn'] ?? function ($action, $headers) {
240 2
                $headers[] = 'SOAPAction: "' . $action . '"';
241
                // 'SOAPAction: "' . $soapAction . '"', pass the soap action in every request from the WSDL if required
242 2
                return $headers;
243
            };
244
        $this->setSoapActionFn($soapActionFn);
245
246
        // cleanup
247
        unset($options['logger']);
248
        unset($options['debugFn']);
249
        unset($options['resFn']);
250
        unset($options['soapActionFn']);
251
252
        parent::__construct($wsdl, $options);
253
    }
254
255
    /**
256
     * Soap __doRequest() Method with CURL Implementation
257
     *
258
     * @param string $request The XML SOAP request
259
     * @param string $location The URL to request
260
     * @param string $action The SOAP action
261
     * @param int $version The SOAP version
262
     * @param int $one_way If one_way is set to 1, this method returns nothing. Use this where a response is not expected
263
     *
264
     * @return string
265
     * @throws \Exception|\SoapFault
266
     */
267 10
    public function __doRequest($request, $location, $action, $version, $one_way = 0)
268
    {
269 10
        $shouldGetResponse = ($this->soapMethod == static::GET_RESPONSE_CONST);
270
271
        // print xml for debugging testing
272 10
        if ($this->logSoapRequest) {
273
            // debug the request here
274 10
            $this->logger->debug($this->formatXml($request));
275
        }
276
277
        // some .NET Servers only accept action method with ns url!! uncomment it if you get error wrong command
278
        /** return the xml response as its coming from normal soap call */
279 10
        if ($shouldGetResponse && $this->xmlResponse) {
280 10
            return $this->xmlResponse;
281
        }
282
283 10
        $soapRequests = &$this->soapRequests;
284
285
        /** @var $id string represent hashId of each request based on the request body
286
         * to avoid multiple calls for the same request if exists
287
         */
288 10
        $id = sha1($location . $request);
289
290
        /** @var $headers array of headers to be sent with request */
291 10
        $this->defaultHeaders['Content-length'] = "Content-length: " . strlen($request);
292
293
        // pass the soap action in every request from the WSDL if required
294 10
        $soapActionFn = $this->soapActionFn;
295 10
        $headers = array_values($soapActionFn($action, $this->defaultHeaders));
296
297
        // ssl connection sharing
298 10
        if (empty($this->sharedCurlData[$location])) {
299 10
            $shOpt = curl_share_init();
300 10
            curl_share_setopt($shOpt, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
301 10
            curl_share_setopt($shOpt, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
302 10
            curl_share_setopt($shOpt, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
303 10
            $this->sharedCurlData[$location] = $shOpt;
304
        }
305
306 10
        $sh = $this->sharedCurlData[$location];
307
308 10
        $ch = curl_init();
309
        /** CURL_OPTIONS  */
310 10
        curl_setopt($ch, CURLOPT_URL, $location);
311 10
        curl_setopt($ch, CURLOPT_POST, true);
312 10
        curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
313 10
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
314 10
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
315 10
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
316 10
        curl_setopt($ch, CURLOPT_SHARE, $sh);
317
318
        // assign curl options
319 10
        foreach ($this->curlOptions as $key => $value) {
320 4
            curl_setopt($ch, $key, $value);
321
        }
322
323 10
        $soapRequests[$id] = $ch;
324
325 10
        $this->requestIds[$id] = $id;
326 10
        $this->soapMethodArr[$id] = $this->soapMethod;
327 10
        $this->requestXmlArr[$id] = $request;
328 10
        $this->lastRequestId = $id;
329
330 10
        return "";
331
    }
332
333
    /**
334
     * Call Sync method, act like normal soap method with extra implementation if needed
335
     * @param string $method
336
     * @param string $args
337
     * @throws \Exception|\SoapFault
338
     * @return string|mixed
339
     */
340 9
    public function callOne($method, $args)
341
    {
342
        try {
343 9
            parent::__call($method, $args);
344
            /** parse the xml response or throw an exception */
345 6
            $this->xmlResponse = $this->run([$this->lastRequestId]);
346 6
            $res = $this->getResponseResult($method, $args);
347 5
        } catch (\Exception $e) {
348 5
            throw $e;
349
        }
350
351 4
        return $res;
352
    }
353
354
    /**
355
     * Call Parallel method, suppress exception and convert to string
356
     * @param string $method
357
     * @param string $args
358
     * @return string|mixed
359
     */
360 6
    public function callMulti($method, $args)
361
    {
362
        /** generate curl session and add the soap requests to execute it later  */
363
        try {
364 6
            parent::__call($method, $args);
365
            /**
366
             * Return the Request ID to the calling method
367
             * This next 2 lines should be custom implementation based on your solution.
368
             *
369
             * @var $res string ,On multiple calls Simulate the response from Soap API to return the request Id of each call
370
             * to be able to get the response with it
371
             * @note check the example file to understand what to write here
372
             */
373 5
            $res = $this->lastRequestId;
374 1
        } catch (\Exception $ex) {
375
            /** catch any SoapFault [is not a valid method for this service] and return null */
376 1
            $res = static::ERROR_STR . ':' . $method . ' - ' . $ex->getCode() . ' - ' . $ex->getMessage() . ' - rand::' . rand();
377
        }
378
379 6
        return $res;
380
    }
381
382
    /**
383
     * __call Magic method to allow one and Parallel Soap calls with exception handling
384
     *
385
     * @param string $method
386
     * @param string $args
387
     *
388
     * @return string|mixed
389
     * @throws \Exception
390
     * @throws \SoapFault
391
     */
392 14
    public function __call($method, $args)
393
    {
394
        /** set current action to the current method call */
395 14
        $this->soapMethod = $method;
396
397 14
        if (!$this->multi) {
398 9
            return $this->callOne($method, $args);
399
        } else {
400 6
            return $this->callMulti($method, $args);
401
        }
402
    }
403
404
    /**
405
     * Execute all or some items from $this->soapRequests
406
     *
407
     * @param mixed $requestIds
408
     * @param bool $partial
409
     */
410 10
    public function doRequests($requestIds = [], $partial = false)
411
    {
412 10
        $allSoapRequests = &$this->soapRequests;
413 10
        $soapResponses = &$this->soapResponses;
414
415
        /** Determine if its partial call to execute some requests or execute all the request in $soapRequests array otherwise */
416 10
        if ($partial) {
417 6
            $soapRequests = array_intersect_key($allSoapRequests, array_flip($requestIds));
418
        } else {
419 5
            $soapRequests = &$this->soapRequests;
420
        }
421
422
        /** Initialise curl multi handler and execute the requests  */
423 10
        $mh = curl_multi_init();
424 10
        foreach ($soapRequests as $ch) {
425 10
            curl_multi_add_handle($mh, $ch);
426
        }
427
428 10
        $active = null;
429
        do {
430 10
            $mrc = curl_multi_exec($mh, $active);
431 10
        } while ($mrc === CURLM_CALL_MULTI_PERFORM || $active);
432
433 10
        while ($active && $mrc == CURLM_OK) {
434
            if (curl_multi_select($mh) != -1) {
435
                do {
436
                    $mrc = curl_multi_exec($mh, $active);
437
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
438
            }
439
        }
440
441
        /** assign the responses for all requests has been performed */
442 10
        foreach ($soapRequests as $id => $ch) {
443
            try {
444 10
                $soapResponses[$id] = curl_multi_getcontent($ch);
445
                // todo if config
446 10
                $curlInfo = curl_getinfo($ch);
447 10
                if ($curlInfo) {
448 10
                    $this->curlInfo[$id] = (object)$curlInfo;
449
                }
450
451
                // @link http://stackoverflow.com/questions/14319696/soap-issue-soapfault-exception-client-looks-like-we-got-no-xml-document
452 10
                if ($soapResponses[$id] === null) {
453 10
                    throw new \SoapFault("HTTP", curl_error($ch));
0 ignored issues
show
curl_error($ch) of type string is incompatible with the type integer expected by parameter $code of SoapFault::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

453
                    throw new \SoapFault("HTTP", /** @scrutinizer ignore-type */ curl_error($ch));
Loading history...
454
                }
455
            } catch (\Exception $e) {
456
                $soapResponses[$id] = $e;
457
            }
458 10
            curl_multi_remove_handle($mh, $ch);
459 10
            curl_close($ch);
460
        }
461 10
        curl_multi_close($mh);
462
463
        /** unset the request performed from the class instance variable $soapRequests so we don't request them again */
464 10
        if (!$partial) {
465 5
            $soapRequests = [];
466
        }
467 10
        foreach ($soapRequests as $id => $ch) {
468 6
            unset($allSoapRequests[$id]);
469
        }
470 10
    }
471
472
    /**
473
     * Main method to perform all or some Soap requests
474
     *
475
     * @param array $requestIds
476
     *
477
     * @return string $res
478
     */
479 10
    public function run($requestIds = [])
480
    {
481 10
        $partial = false;
482
483 10
        if (is_array($requestIds) && count($requestIds)) {
484 6
            $partial = true;
485
        }
486 10
        $allSoapResponses = &$this->soapResponses;
487
488
        /** perform all the request */
489 10
        $this->doRequests($requestIds, $partial);
490
491
        /** reset the class to synchronous mode */
492 10
        $this->setMulti(false);
493
494
        /** parse return response of the performed requests  */
495 10
        if ($partial) {
496 6
            $soapResponses = array_intersect_key($allSoapResponses, array_flip($requestIds));
497
        } else {
498 5
            $soapResponses = &$this->soapResponses;
499
        }
500
        /** if its one request return the first element in the array */
501 10
        if ($partial && count($requestIds) == 1) {
502 6
            $res = $soapResponses[$requestIds[0]];
503 6
            unset($allSoapResponses[$requestIds[0]]);
504
        } else {
505 5
            $res = $this->getMultiResponses($soapResponses);
506
        }
507
508 10
        return $res;
509
    }
510
511
    /**
512
     * Parse Response of Soap Requests with parent::__doRequest()
513
     *
514
     * @param array $responses
515
     *
516
     * @return mixed $resArr
517
     */
518 5
    public function getMultiResponses($responses = [])
519
    {
520 5
        $resArr = [];
521 5
        $this->soapMethod = static::GET_RESPONSE_CONST;
522
523 5
        foreach ($responses as $id => $ch) {
524
            try {
525 5
                $this->xmlResponse = $ch;
526 5
                if ($ch instanceof \Exception) {
527
                    throw $ch;
528
                }
529 5
                $res = parent::__call($this->soapMethodArr[$id], []);
0 ignored issues
show
array() of type array is incompatible with the type string expected by parameter $arguments of SoapClient::__call(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

529
                $res = parent::__call($this->soapMethodArr[$id], /** @scrutinizer ignore-type */ []);
Loading history...
530
                /**
531
                 * Return the Request ID to the calling method
532
                 * This next lines should be custom implementation based on your solution.
533
                 *
534
                 * @var $resArr string ,On multiple calls Simulate the response from Soap API to return the request Id of each call
535
                 * to be able to get the response with it
536
                 * @note check the example file to understand what to write here
537
                 */
538 5
                $resFn = $this->resFn;
539 5
                $resArr[$id] = $resFn($this->soapMethodArr[$id], $res);
540 5
                $this->addDebugData($res, $id);
541 1
            } catch (\Exception $ex) {
542 1
                $this->addDebugData($ex, $this->lastRequestId);
543 1
                $resArr[$id] = $ex;
544
            }
545 5
            unset($this->soapResponses[$id]);
546
        }
547 5
        $this->xmlResponse = '';
548 5
        $this->soapMethod = '';
549
550 5
        return $resArr;
551
    }
552
553
    /**
554
     * Parse Response of Soap Requests with parent::__doRequest()
555
     *
556
     * @param string $method
557
     * @param string|array $args
558
     *
559
     * @throws \Exception|\SoapFault|
560
     * @return string $res
561
     */
562 6
    public function getResponseResult($method, $args)
563
    {
564 6
        $this->soapMethod = static::GET_RESPONSE_CONST;
565
566
        try {
567 6
            $res = parent::__call($method, $args);
0 ignored issues
show
It seems like $args can also be of type array; however, parameter $arguments of SoapClient::__call() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

567
            $res = parent::__call($method, /** @scrutinizer ignore-type */ $args);
Loading history...
568
569 4
            $id = $this->lastRequestId;
570 4
            $this->addDebugData($res, $id);
571 2
        } catch (\Exception $ex) {
572
            // todo find better pattern for handling non-xml document with error handling
573
            // if (strtolower($ex->getMessage()) == 'looks like we got no XML document') {}
574 2
            $this->addDebugData($ex, $this->lastRequestId);
575 2
            throw $ex;
576
        }
577 4
        $this->soapMethod = '';
578
579 4
        $resFn = $this->resFn;
580 4
        return $resFn($method, $res);
581
    }
582
583
584
    /**
585
     * Add curl info to response object
586
     *
587
     * @param $res
588
     * @param $id
589
     *
590
     * @author Mohamed Meabed <[email protected]>
591
     * @return mixed
592
     */
593 10
    public function addDebugData($res, $id)
594
    {
595 10
        $fn = $this->debugFn;
596 10
        return $fn($res, $id);
597
    }
598
599
    /**
600
     * format xml
601
     *
602
     * @param $request
603
     * @author Mohamed Meabed <[email protected]>
604
     * @return mixed
605
     */
606 10
    public function formatXml($request)
607
    {
608 10
        $fn = $this->formatXmlFn;
609 10
        return $fn($request);
610
    }
611
}
612