Passed
Push — master ( fd3a2c...c2444c )
by Mohamed
01:22
created

ParallelSoapClient::getFormatXmlFn()   A

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\LoggerInterface;
6
use Psr\Log\NullLogger;
7
8
/**
9
 * Single/Parallel Soap Class
10
 *
11
 * Implements soap with multi server-to-server calls using curl module.
12
 *
13
 * @author Mohamed Meabed <[email protected]>
14
 * @link   https://github.com/Meabed/php-parallel-soap
15
 * @note   Check the Example files and read the documentation carefully
16
 */
17
class ParallelSoapClient extends \SoapClient
18
{
19
    /** @var array of all responses in the client */
20
    public $soapResponses = [];
21
22
    /** @var array of all requests in the client */
23
    public $soapRequests = [];
24
25
    /** @var array of all requests xml in the client */
26
    public $requestXmlArr = [];
27
28
    /** @var string the xml returned from soap call */
29
    public $xmlResponse;
30
31
    /** @var string current method call */
32
    public $soapMethod;
33
34
    /** @var array of all requests soap methods in the client */
35
    public $soapMethodArr = [];
36
37
    /** @var array of all requestIds */
38
    public $requestIds;
39
40
    /** @var string last request id */
41
    public $lastRequestId;
42
43
    /** @var bool soap parallel flag */
44
    public $multi = false;
45
46
    /** @var bool log soap request */
47
    public $logSoapRequest = false;
48
49
    /** @var array of all curl_info in the client */
50
    public $curlInfo = [];
51
52
    /** @var array curl options */
53
    public $curlOptions = [];
54
55
    /** @var \Closure */
56
    protected $debugFn;
57
58
    /** @var \Closure */
59
    protected $formatXmlFn;
60
61
    /** @var \Closure */
62
    protected $resFn;
63
64
    /** @var \Closure */
65
    protected $soapActionFn;
66
67
    /** @var LoggerInterface */
68
    public $logger;
69
70
    /** @var array curl share ssl */
71
    public $sharedCurlData = [];
72
73
    /** @var string getRequestResponse action constant used for parsing the xml with from parent::__doRequest */
74
    const GET_RESPONSE_CONST = 'getRequestResponseMethod';
75
    /** @var string text prefix, if error happen due to SOAP error before its executed ex:invalid method */
76
    const ERROR_STR = '*ERROR*';
77
78
    /**
79
     * @return mixed
80
     */
81
    public function getMulti()
82
    {
83
        return $this->multi;
84
    }
85
86
    /**
87
     * @param mixed $multi
88
     * @return ParallelSoapClient
89
     */
90 8
    public function setMulti($multi)
91
    {
92 8
        $this->multi = $multi;
93 8
        return $this;
94
    }
95
96
    /**
97
     * @return array
98
     */
99
    public function getCurlOptions()
100
    {
101
        return $this->curlOptions;
102
    }
103
104
    /**
105
     * @param array $curlOptions
106
     * @return ParallelSoapClient
107
     */
108
    public function setCurlOptions(array $curlOptions)
109
    {
110
        $this->curlOptions = $curlOptions;
111
        return $this;
112
    }
113
114
    /**
115
     * @return LoggerInterface
116
     */
117
    public function getLogger()
118
    {
119
        return $this->logger;
120
    }
121
122
    /**
123
     * @param LoggerInterface $logger
124
     * @return ParallelSoapClient
125
     */
126
    public function setLogger(LoggerInterface $logger)
127
    {
128
        $this->logger = $logger;
129
        return $this;
130
    }
131
132
    /**
133
     * @return bool
134
     */
135
    public function isLogSoapRequest()
136
    {
137
        return $this->logSoapRequest;
138
    }
139
140
    /**
141
     * @param bool $logSoapRequest
142
     * @return ParallelSoapClient
143
     */
144
    public function setLogSoapRequest(bool $logSoapRequest)
145
    {
146
        $this->logSoapRequest = $logSoapRequest;
147
        return $this;
148
    }
149
150
    /**
151
     * @return \Closure
152
     */
153
    public function getDebugFn()
154
    {
155
        return $this->debugFn;
156
    }
157
158
    /**
159
     * @param \Closure $debugFn
160
     * @return ParallelSoapClient
161
     */
162
    public function setDebugFn(\Closure $debugFn)
163
    {
164
        $this->debugFn = $debugFn;
165
        return $this;
166
    }
167
168
    /**
169
     * @return \Closure
170
     */
171
    public function getFormatXmlFn()
172
    {
173
        return $this->formatXmlFn;
174
    }
175
176
    /**
177
     * @param \Closure $formatXmlFn
178
     * @return ParallelSoapClient
179
     */
180
    public function setFormatXmlFn(\Closure $formatXmlFn)
181
    {
182
        $this->formatXmlFn = $formatXmlFn;
183
        return $this;
184
    }
185
186
    /**
187
     * @return \Closure
188
     */
189
    public function getResFn()
190
    {
191
        return $this->resFn;
192
    }
193
194
    /**
195
     * @param \Closure $resFn
196
     * @return ParallelSoapClient
197
     */
198
    public function setResFn(\Closure $resFn)
199
    {
200
        $this->resFn = $resFn;
201
        return $this;
202
    }
203
204
    /**
205
     * @return \Closure
206
     */
207
    public function getSoapActionFn()
208
    {
209
        return $this->soapActionFn;
210
    }
211
212
    /**
213
     * @param \Closure $soapActionFn
214
     * @return ParallelSoapClient
215
     */
216
    public function setSoapActionFn(\Closure $soapActionFn)
217
    {
218
        $this->soapActionFn = $soapActionFn;
219
        return $this;
220
    }
221
222
223 8
    public function __construct($wsdl, array $options = null)
224
    {
225
        // logger
226
        $logger = $options['logger'] ?? new NullLogger();
227
        $this->setLogger($logger);
228
229
        // debug function to add headers / last request / response / etc...
230 8
        $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

230
        $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

230
        $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...
231 8
            };
232
        $this->setDebugFn($debugFn);
233
234
        // format xml before logging
235
        $formatXmlFn = $options['formatXmlFn'] ?? function ($xml) {
236
                return $xml;
237
            };
238
        $this->setFormatXmlFn($formatXmlFn);
239
240
        // result parsing function
241
        $resFn = $options['resFn'] ?? function ($method, $res) {
242
                return $res;
243
            };
244
        $this->setResFn($resFn);
245
246
        // soapAction function to set in the header
247
        // Ex: SOAPAction: "http://tempuri.org/SOAP.Demo.AddInteger"
248
        // Ex: SOAPAction: "http://webservices.amadeus.com/PNRRET_11_3_1A"
249
        $soapActionFn = $options['soapActionFn'] ?? function ($action, $headers) {
250
                $headers[] = 'SOAPAction: "' . $action . '"';
251
                // 'SOAPAction: "' . $soapAction . '"', pass the soap action in every request from the WSDL if required
252
                return $headers;
253
            };
254
        $this->setSoapActionFn($soapActionFn);
255
256
        // cleanup
257
        unset($options['logger']);
258
        unset($options['debugFn']);
259
        unset($options['resFn']);
260
        unset($options['soapActionFn']);
261
262
        parent::__construct($wsdl, $options);
263
    }
264
265
    /**
266
     * Soap __doRequest() Method with CURL Implementation
267
     *
268
     * @param string $request The XML SOAP request
269
     * @param string $location The URL to request
270
     * @param string $action The SOAP action
271
     * @param int $version The SOAP version
272
     * @param int $one_way If one_way is set to 1, this method returns nothing. Use this where a response is not expected
273
     *
274
     * @return string
275
     * @throws \Exception|\SoapFault
276
     */
277 8
    public function __doRequest($request, $location, $action, $version, $one_way = 0)
278
    {
279 8
        $shouldGetResponse = ($this->soapMethod == static::GET_RESPONSE_CONST);
280
281
        // print xml for debugging testing
282 8
        if ($this->logSoapRequest) {
283
            // debug the request here
284 6
            $this->logger->debug($this->formatXml($request));
285
        }
286
        
287
        // some .NET Servers only accept action method with ns url!! uncomment it if you get error wrong command
288
        /** return the xml response as its coming from normal soap call */
289 8
        if ($shouldGetResponse && $this->xmlResponse) {
290 8
            return $this->xmlResponse;
291
        }
292
293 8
        $soapRequests = &$this->soapRequests;
294
295
        /** @var $id string represent hashId of each request based on the request body
296
         * to avoid multiple calls for the same request if exists
297
         */
298 8
        $id = sha1($location . $request);
299
300
        /** @var $headers array of headers to be sent with request */
301
        $defaultHeaders = [
302 8
            'Content-type: text/xml',
303 8
            'charset=utf-8',
304 8
            "Accept: text/xml",
305 8
            "Content-length: " . strlen($request),
306
        ];
307
308
        // pass the soap action in every request from the WSDL if required
309 8
        $soapActionFn = $this->soapActionFn;
310 8
        $headers = $soapActionFn($action, $defaultHeaders);
311
312
        // ssl connection sharing
313 8
        if (empty($this->sharedCurlData[$location])) {
314 8
            $shOpt = curl_share_init();
315 8
            curl_share_setopt($shOpt, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
316 8
            curl_share_setopt($shOpt, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
317 8
            curl_share_setopt($shOpt, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
318 8
            $this->sharedCurlData[$location] = $shOpt;
319
        }
320
321 8
        $sh = $this->sharedCurlData[$location];
322
323 8
        $ch = curl_init();
324
        /** CURL_OPTIONS  */
325 8
        curl_setopt($ch, CURLOPT_URL, $location);
326 8
        curl_setopt($ch, CURLOPT_POST, true);
327 8
        curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
328 8
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
329 8
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
330 8
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
331 8
        curl_setopt($ch, CURLOPT_SHARE, $sh);
332
333
        // assign curl options
334 8
        foreach ($this->curlOptions as $key => $value) {
335 3
            curl_setopt($ch, $key, $value);
336
        }
337
338 8
        $soapRequests[$id] = $ch;
339
340 8
        $this->requestIds[$id] = $id;
341 8
        $this->soapMethodArr[$id] = $this->soapMethod;
342 8
        $this->requestXmlArr[$id] = $request;
343 8
        $this->lastRequestId = $id;
344
345 8
        return "";
346
    }
347
348
    /**
349
     * Call Sync method, act like normal soap method with extra implementation if needed
350
     * @param string $method
351
     * @param string $args
352
     * @throws \Exception|\SoapFault
353
     * @return string|mixed
354
     */
355 8
    public function callOne($method, $args)
356
    {
357
        try {
358 8
            parent::__call($method, $args);
359
            /** parse the xml response or throw an exception */
360 5
            $this->xmlResponse = $this->run([$this->lastRequestId]);
361 5
            $res = $this->getResponseResult($method, $args);
362 4
        } catch (\Exception $e) {
363 4
            throw $e;
364
        }
365
366 4
        return $res;
367
    }
368
369
    /**
370
     * Call Parallel method, suppress exception and convert to string
371
     * @param string $method
372
     * @param string $args
373
     * @return string|mixed
374
     */
375 5
    public function callParallel($method, $args)
376
    {
377
        /** generate curl session and add the soap requests to execute it later  */
378
        try {
379 5
            parent::__call($method, $args);
380
            /**
381
             * Return the Request ID to the calling method
382
             * This next 2 lines should be custom implementation based on your solution.
383
             *
384
             * @var $res string ,On multiple calls Simulate the response from Soap API to return the request Id of each call
385
             * to be able to get the response with it
386
             * @note check the example file to understand what to write here
387
             */
388 4
            $res = $this->lastRequestId;
389 1
        } catch (\Exception $ex) {
390
            /** catch any SoapFault [is not a valid method for this service] and return null */
391 1
            $res = static::ERROR_STR . ':' . $method . ' - ' . $ex->getCode() . ' - ' . $ex->getMessage() . ' - rand::' . rand();
392
        }
393
394 5
        return $res;
395
    }
396
397
    /**
398
     * __call Magic method to allow one and Parallel Soap calls with exception handling
399
     *
400
     * @param string $method
401
     * @param string $args
402
     *
403
     * @return string|mixed
404
     * @throws \Exception
405
     * @throws \SoapFault
406
     */
407 12
    public function __call($method, $args)
408
    {
409
        /** set current action to the current method call */
410 12
        $this->soapMethod = $method;
411
412 12
        if (!$this->multi) {
413 8
            return $this->callOne($method, $args);
414
        } else {
415 5
            return $this->callParallel($method, $args);
416
        }
417
    }
418
419
    /**
420
     * Execute all or some items from $this->soapRequests
421
     *
422
     * @param mixed $requestIds
423
     * @param bool $partial
424
     */
425 8
    public function doRequests($requestIds = [], $partial = false)
426
    {
427 8
        $allSoapRequests = &$this->soapRequests;
428 8
        $soapResponses = &$this->soapResponses;
429
430
        /** Determine if its partial call to execute some requests or execute all the request in $soapRequests array otherwise */
431 8
        if ($partial) {
432 5
            $soapRequests = array_intersect_key($allSoapRequests, array_flip($requestIds));
433
        } else {
434 4
            $soapRequests = &$this->soapRequests;
435
        }
436
437
        /** Initialise curl multi handler and execute the requests  */
438 8
        $mh = curl_multi_init();
439 8
        foreach ($soapRequests as $ch) {
440 8
            curl_multi_add_handle($mh, $ch);
441
        }
442
443 8
        $active = null;
444
        do {
445 8
            $mrc = curl_multi_exec($mh, $active);
446 8
        } while ($mrc === CURLM_CALL_MULTI_PERFORM || $active);
447
448 8
        while ($active && $mrc == CURLM_OK) {
449
            if (curl_multi_select($mh) != -1) {
450
                do {
451
                    $mrc = curl_multi_exec($mh, $active);
452
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
453
            }
454
        }
455
        /** assign the responses for all requests has been performed */
456 8
        foreach ($soapRequests as $id => $ch) {
457
            try {
458 8
                $soapResponses[$id] = curl_multi_getcontent($ch);
459
                // todo if config
460 8
                $curlInfo = curl_getinfo($ch);
461 8
                if ($curlInfo) {
462 8
                    $this->curlInfo[$id] = (object)$curlInfo;
463
                }
464
465
                // @link http://stackoverflow.com/questions/14319696/soap-issue-soapfault-exception-client-looks-like-we-got-no-xml-document
466 8
                if ($soapResponses[$id] === null) {
467 8
                    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

467
                    throw new \SoapFault("HTTP", /** @scrutinizer ignore-type */ curl_error($ch));
Loading history...
468
                }
469
            } catch (\Exception $e) {
470
                $soapResponses[$id] = $e;
471
            }
472 8
            curl_multi_remove_handle($mh, $ch);
473 8
            curl_close($ch);
474
        }
475 8
        curl_multi_close($mh);
476
477
        /** unset the request performed from the class instance variable $soapRequests so we don't request them again */
478 8
        if (!$partial) {
479 4
            $soapRequests = [];
480
        }
481 8
        foreach ($soapRequests as $id => $ch) {
482 5
            unset($allSoapRequests[$id]);
483
        }
484 8
    }
485
486
    /**
487
     * Main method to perform all or some Soap requests
488
     *
489
     * @param array $requestIds
490
     *
491
     * @return string $res
492
     */
493 8
    public function run($requestIds = [])
494
    {
495 8
        $partial = false;
496
497 8
        if (is_array($requestIds) && count($requestIds)) {
498 5
            $partial = true;
499
        }
500 8
        $allSoapResponses = &$this->soapResponses;
501
502
        /** perform all the request */
503 8
        $this->doRequests($requestIds, $partial);
504
505
        /** reset the class to synchronous mode */
506 8
        $this->setMulti(false);
507
508
        /** parse return response of the performed requests  */
509 8
        if ($partial) {
510 5
            $soapResponses = array_intersect_key($allSoapResponses, array_flip($requestIds));
511
        } else {
512 4
            $soapResponses = &$this->soapResponses;
513
        }
514
        /** if its one request return the first element in the array */
515 8
        if ($partial && count($requestIds) == 1) {
516 5
            $res = $soapResponses[$requestIds[0]];
517 5
            unset($allSoapResponses[$requestIds[0]]);
518
        } else {
519 4
            $res = $this->getMultiResponses($soapResponses);
520
        }
521
522 8
        return $res;
523
    }
524
525
    /**
526
     * Parse Response of Soap Requests with parent::__doRequest()
527
     *
528
     * @param array $responses
529
     *
530
     * @return mixed $resArr
531
     */
532 4
    public function getMultiResponses($responses = [])
533
    {
534 4
        $resArr = [];
535 4
        $this->soapMethod = static::GET_RESPONSE_CONST;
536
537 4
        foreach ($responses as $id => $ch) {
538
            try {
539 4
                $this->xmlResponse = $ch;
540 4
                if ($ch instanceof \Exception) {
541
                    throw $ch;
542
                }
543 4
                $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

543
                $res = parent::__call($this->soapMethodArr[$id], /** @scrutinizer ignore-type */ []);
Loading history...
544
                /**
545
                 * Return the Request ID to the calling method
546
                 * This next lines should be custom implementation based on your solution.
547
                 *
548
                 * @var $resArr string ,On multiple calls Simulate the response from Soap API to return the request Id of each call
549
                 * to be able to get the response with it
550
                 * @note check the example file to understand what to write here
551
                 */
552 4
                $resFn = $this->resFn;
553 4
                $resArr[$id] = $resFn($this->soapMethodArr[$id], $res);
554 4
                $this->addDebugData($res, $id);
555
            } catch (\Exception $ex) {
556
                $this->addDebugData($ex, $this->lastRequestId);
557
                $resArr[$id] = $ex;
558
            }
559 4
            unset($this->soapResponses[$id]);
560
        }
561 4
        $this->xmlResponse = '';
562 4
        $this->soapMethod = '';
563
564 4
        return $resArr;
565
    }
566
567
    /**
568
     * Parse Response of Soap Requests with parent::__doRequest()
569
     *
570
     * @param string $method
571
     * @param string|array $args
572
     *
573
     * @throws \Exception|\SoapFault|
574
     * @return string $res
575
     */
576 5
    public function getResponseResult($method, $args)
577
    {
578 5
        $this->soapMethod = static::GET_RESPONSE_CONST;
579
580
        try {
581 5
            $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

581
            $res = parent::__call($method, /** @scrutinizer ignore-type */ $args);
Loading history...
582
583 4
            $id = $this->lastRequestId;
584 4
            $this->addDebugData($res, $id);
585 1
        } catch (\Exception $ex) {
586 1
            $this->addDebugData($ex, $this->lastRequestId);
587 1
            throw $ex;
588
        }
589 4
        $this->soapMethod = '';
590
591 4
        $resFn = $this->resFn;
592 4
        return $resFn($method, $res);
593
    }
594
595
596
    /**
597
     * Add curl info to response object
598
     *
599
     * @param $res
600
     * @param $id
601
     *
602
     * @author Mohamed Meabed <[email protected]>
603
     * @return mixed
604
     */
605 8
    public function addDebugData($res, $id)
606
    {
607 8
        $fn = $this->debugFn;
608 8
        return $fn($res, $id);
609
    }
610
611
    /**
612
     * format xml
613
     *
614
     * @param $request
615
     * @author Mohamed Meabed <[email protected]>
616
     * @return mixed
617
     */
618 6
    public function formatXml($request)
619
    {
620 6
        $fn = $this->formatXmlFn;
621 6
        return $fn($request);
622
    }
623
}
624