ParallelSoapClient::getMulti()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed. ( Ignorable by Annotation )

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

220
        $debugFn = $options['debugFn'] ?? function (/** @scrutinizer ignore-unused */ $res, $id) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $id is not used and could be removed. ( Ignorable by Annotation )

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

220
        $debugFn = $options['debugFn'] ?? function ($res, /** @scrutinizer ignore-unused */ $id) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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') {}
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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