Completed
Push — master ( a7e5d3...b7c304 )
by Mohamed
01:34
created

ParallelSoapClient::addDebugData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
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
    /**  array of all responses in the client */
20
    public $soapResponses = [];
21
22
    /**  array of all requests in the client */
23
    public $soapRequests = [];
24
25
    /**  array of all requests xml in the client */
26
    public $requestXmlArr = [];
27
28
    /**  string the xml returned from soap call */
29
    public $xmlResponse;
30
31
    /**  string current method call  */
32
    public $soapMethod;
33
34
    /**  array of all requests soap methods in the client */
35
    public $soapMethodArr = [];
36
37
    /**  array of all requestIds  */
38
    public $requestIds;
39
40
    /**  string last request id  */
41
    public $lastRequestId;
42
43
    /**  bool soap parallel flag  */
44
    public $multi = false;
45
46
    /** @var bool log soap request */
47
    public $logSoapRequest = false;
48
49
    /** @var bool log pretty xml */
50
    public $logPrettyXml = false;
51
52
    /**  array of all curl_info in the client */
53
    public $curlInfo = [];
54
55
    /** @var array curl options */
56
    public $curlOptions = [];
57
58
    /** @var \Closure */
59
    public $debugFn;
60
61
    /** @var \Closure */
62
    public $resFn;
63
64
    /** @var \Closure */
65
    public $soapActionFn;
66
67
    /** @var LoggerInterface */
68
    public $logger;
69
70
    /**
71
     * @var array
72
     */
73
    public $sharedCurlData = [];
74
75
    /**  getRequestResponse action constant used for parsing the xml with from parent::__doRequest */
76
    const GET_RESPONSE_CONST = 'getRequestResponseMethod';
77
    const ERROR_STR = '*ERROR*';
78
79
80
    /**
81
     * @return mixed
82
     */
83
    public function getMulti()
84
    {
85
        return $this->multi;
86
    }
87
88
    /**
89
     * @param mixed $multi
90
     * @return ParallelSoapClient
91
     */
92 6
    public function setMulti($multi)
93
    {
94 6
        $this->multi = $multi;
95 6
        return $this;
96
    }
97
98
    /**
99
     * @return array
100
     */
101
    public function getCurlOptions()
102
    {
103
        return $this->curlOptions;
104
    }
105
106
    /**
107
     * @param array $curlOptions
108
     * @return ParallelSoapClient
109
     */
110
    public function setCurlOptions(array $curlOptions)
111
    {
112
        $this->curlOptions = $curlOptions;
113
        return $this;
114
    }
115
116
    /**
117
     * @return LoggerInterface
118
     */
119
    public function getLogger()
120
    {
121
        return $this->logger;
122
    }
123
124
    /**
125
     * @param LoggerInterface $logger
126
     * @return ParallelSoapClient
127
     */
128
    public function setLogger(LoggerInterface $logger)
129
    {
130
        $this->logger = $logger;
131
        return $this;
132
    }
133
134
    /**
135
     * @return bool
136
     */
137
    public function isLogSoapRequest()
138
    {
139
        return $this->logSoapRequest;
140
    }
141
142
    /**
143
     * @param bool $logSoapRequest
144
     * @return ParallelSoapClient
145
     */
146
    public function setLogSoapRequest(bool $logSoapRequest)
147
    {
148
        $this->logSoapRequest = $logSoapRequest;
149
        return $this;
150
    }
151
152
    /**
153
     * @return bool
154
     */
155
    public function isLogPrettyXml()
156
    {
157
        return $this->logPrettyXml;
158
    }
159
160
    /**
161
     * @param bool $logPrettyXml
162
     * @return ParallelSoapClient
163
     */
164
    public function setLogPrettyXml(bool $logPrettyXml)
165
    {
166
        $this->logPrettyXml = $logPrettyXml;
167
        return $this;
168
    }
169
170
    /**
171
     * @return \Closure
172
     */
173
    public function getDebugFn()
174
    {
175
        return $this->debugFn;
176
    }
177
178
    /**
179
     * @param \Closure $debugFn
180
     * @return ParallelSoapClient
181
     */
182
    public function setDebugFn(\Closure $debugFn)
183
    {
184
        $this->debugFn = $debugFn;
185
        return $this;
186
    }
187
188
189 6
    public function __construct($wsdl, array $options = null)
190
    {
191
        parent::__construct($wsdl, $options);
192
        $this->logger = $options['logger'] ?? new NullLogger();
193 6
        $this->debugFn = $options['debugFn'] ?? function ($res, $id) {
0 ignored issues
show
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

193
        $this->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...
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

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

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

526
                $res = parent::__call($this->soapMethodArr[$id], /** @scrutinizer ignore-type */ []);
Loading history...
527
                /**
528
                 * Return the Request ID to the calling method
529
                 * This next lines should be custom implementation based on your solution.
530
                 *
531
                 * @var $resArr string ,On multiple calls Simulate the response from Soap API to return the request Id of each call
532
                 * to be able to get the response with it
533
                 * @note check the example file to understand what to write here
534
                 */
535 2
                $resFn = $this->resFn;
536 2
                $resArr[$id] = $resFn($this->soapMethodArr[$id], $res);
537 2
                $this->addDebugData($res, $id);
538
            } catch (\SoapFault $ex) {
539
                $this->addDebugData($ex, $this->lastRequestId);
540
                $resArr[$id] = $ex;
541
            }
542 2
            unset($this->soapResponses[$id]);
543
        }
544 2
        $this->xmlResponse = '';
545 2
        $this->soapMethod = '';
546
547 2
        return $resArr;
548
    }
549
550
    /**
551
     * Parse Response of Soap Requests with parent::__doRequest()
552
     *
553
     * @param string $method
554
     * @param string|array $args
555
     *
556
     * @throws \SoapFault
557
     * @throws \Exception
558
     * @return string $res
559
     */
560 6
    public function getResponseResult($method, $args)
561
    {
562 6
        $this->soapMethod = static::GET_RESPONSE_CONST;
563
564
        try {
565 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

565
            $res = parent::__call($method, /** @scrutinizer ignore-type */ $args);
Loading history...
566
567 5
            $id = $this->lastRequestId;
568 5
            $this->addDebugData($res, $id);
569 1
        } catch (\SoapFault $ex) {
570 1
            $this->addDebugData($ex, $this->lastRequestId);
571 1
            throw $ex;
572
        } catch (\Exception $ex) {
573
            $this->addDebugData($ex, $this->lastRequestId);
574
            throw $ex;
575
        }
576 5
        $this->soapMethod = '';
577
578 5
        $resFn = $this->resFn;
579 5
        return $resFn($method, $res);
580
    }
581
582
583
    /**
584
     * Add curl info to response object
585
     *
586
     * @param $res
587
     * @param $id
588
     *
589
     * @author Mohamed Meabed <[email protected]>
590
     *
591
     */
592 6
    public function addDebugData($res, $id)
593
    {
594 6
        $fn = $this->debugFn;
595 6
        $fn($res, $id);
596 6
    }
597
598
    /**
599
     * Print pretty xml
600
     *
601
     * @param $request
602
     *
603
     * @return string
604
     *
605
     * @author Mohamed Meabed <[email protected]>
606
     *
607
     */
608
    public function prettyXml($request)
609
    {
610
        $dom = new \DOMDocument();
611
        $dom->preserveWhiteSpace = false;
612
        $dom->loadXML($request);
613
        $dom->formatOutput = true;
614
615
        return $dom->saveXml();
616
    }
617
}
618