Support::toXml()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4555
c 0
b 0
f 0
cc 5
nc 4
nop 1
1
<?php
2
3
namespace Yansongda\Pay\Gateways\Wechat;
4
5
use Exception;
6
use Yansongda\Pay\Events;
7
use Yansongda\Pay\Exceptions\BusinessException;
8
use Yansongda\Pay\Exceptions\GatewayException;
9
use Yansongda\Pay\Exceptions\InvalidArgumentException;
10
use Yansongda\Pay\Exceptions\InvalidSignException;
11
use Yansongda\Pay\Gateways\Wechat;
12
use Yansongda\Pay\Log;
13
use Yansongda\Supports\Collection;
14
use Yansongda\Supports\Config;
15
use Yansongda\Supports\Str;
16
use Yansongda\Supports\Traits\HasHttpRequest;
17
18
/**
19
 * @author yansongda <[email protected]>
20
 *
21
 * @property string appid
22
 * @property string app_id
23
 * @property string miniapp_id
24
 * @property string sub_appid
25
 * @property string sub_app_id
26
 * @property string sub_miniapp_id
27
 * @property string mch_id
28
 * @property string sub_mch_id
29
 * @property string key
30
 * @property string return_url
31
 * @property string cert_client
32
 * @property string cert_key
33
 * @property array log
34
 * @property array http
35
 * @property string mode
36
 */
37
class Support
38
{
39
    use HasHttpRequest;
40
41
    /**
42
     * Wechat gateway.
43
     *
44
     * @var string
45
     */
46
    protected $baseUri;
47
48
    /**
49
     * Config.
50
     *
51
     * @var Config
52
     */
53
    protected $config;
54
55
    /**
56
     * Instance.
57
     *
58
     * @var Support
59
     */
60
    private static $instance;
61
62
    /**
63
     * Bootstrap.
64
     *
65
     * @author yansongda <[email protected]>
66
     *
67
     * @param Config $config
68
     */
69 View Code Duplication
    private function __construct(Config $config)
0 ignored issues
show
Duplication introduced by yansongda
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
70
    {
71
        $this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)];
72
        $this->config = $config;
73
74
        $this->setHttpOptions();
75
    }
76
77
    /**
78
     * __get.
79
     *
80
     * @author yansongda <[email protected]>
81
     *
82
     * @param $key
83
     *
84
     * @return mixed|Config|null
85
     */
86
    public function __get($key)
87
    {
88
        return $this->getConfig($key);
89
    }
90
91
    /**
92
     * create.
93
     *
94
     * @author yansongda <[email protected]>
95
     *
96
     * @param Config $config
97
     *
98
     * @throws GatewayException
99
     * @throws InvalidArgumentException
100
     * @throws InvalidSignException
101
     *
102
     * @return Support
103
     */
104 View Code Duplication
    public static function create(Config $config)
0 ignored issues
show
Duplication introduced by yansongda
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
105
    {
106
        if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
107
            self::$instance = new self($config);
108
109
            self::setDevKey();
110
        }
111
112
        return self::$instance;
113
    }
114
115
    /**
116
     * getInstance.
117
     *
118
     * @author yansongda <[email protected]>
119
     *
120
     * @throws InvalidArgumentException
121
     *
122
     * @return Support
123
     */
124
    public static function getInstance()
125
    {
126
        if (is_null(self::$instance)) {
127
            throw new InvalidArgumentException('You Should [Create] First Before Using');
128
        }
129
130
        return self::$instance;
131
    }
132
133
    /**
134
     * clear.
135
     *
136
     * @author yansongda <[email protected]>
137
     */
138
    public static function clear()
139
    {
140
        self::$instance = null;
141
    }
142
143
    /**
144
     * Request wechat api.
145
     *
146
     * @author yansongda <[email protected]>
147
     *
148
     * @param string $endpoint
149
     * @param array  $data
150
     * @param bool   $cert
151
     *
152
     * @throws GatewayException
153
     * @throws InvalidArgumentException
154
     * @throws InvalidSignException
155
     *
156
     * @return Collection
157
     */
158
    public static function requestApi($endpoint, $data, $cert = false): Collection
159
    {
160
        Events::dispatch(new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri().$endpoint, $data));
161
162
        $result = self::$instance->post(
163
            $endpoint,
164
            self::toXml($data),
165
            $cert ? [
166
                'cert' => self::$instance->cert_client,
167
                'ssl_key' => self::$instance->cert_key,
168
            ] : []
169
        );
170
        $result = is_array($result) ? $result : self::fromXml($result);
171
172
        Events::dispatch(new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri().$endpoint, $result));
173
174
        return self::processingApiResult($endpoint, $result);
175
    }
176
177
    /**
178
     * Filter payload.
179
     *
180
     * @author yansongda <[email protected]>
181
     *
182
     * @param array        $payload
183
     * @param array|string $params
184
     * @param bool         $preserve_notify_url
185
     *
186
     * @throws InvalidArgumentException
187
     *
188
     * @return array
189
     */
190
    public static function filterPayload($payload, $params, $preserve_notify_url = false): array
191
    {
192
        $type = self::getTypeName($params['type'] ?? '');
193
194
        $payload = array_merge(
195
            $payload,
196
            is_array($params) ? $params : ['out_trade_no' => $params]
197
        );
198
        $payload['appid'] = self::$instance->getConfig($type, '');
199
200
        if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) {
201
            $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');
202
        }
203
204
        unset($payload['trade_type'], $payload['type']);
205
        if (!$preserve_notify_url) {
206
            unset($payload['notify_url']);
207
        }
208
209
        $payload['sign'] = self::generateSign($payload);
210
211
        return $payload;
212
    }
213
214
    /**
215
     * Generate wechat sign.
216
     *
217
     * @author yansongda <[email protected]>
218
     *
219
     * @param array $data
220
     *
221
     * @throws InvalidArgumentException
222
     *
223
     * @return string
224
     */
225
    public static function generateSign($data): string
226
    {
227
        $key = self::$instance->key;
228
229
        if (is_null($key)) {
230
            throw new InvalidArgumentException('Missing Wechat Config -- [key]');
231
        }
232
233
        ksort($data);
234
235
        $string = md5(self::getSignContent($data).'&key='.$key);
236
237
        Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]);
238
239
        return strtoupper($string);
240
    }
241
242
    /**
243
     * Generate sign content.
244
     *
245
     * @author yansongda <[email protected]>
246
     *
247
     * @param array $data
248
     *
249
     * @return string
250
     */
251
    public static function getSignContent($data): string
252
    {
253
        $buff = '';
254
255
        foreach ($data as $k => $v) {
256
            $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
257
        }
258
259
        Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]);
260
261
        return trim($buff, '&');
262
    }
263
264
    /**
265
     * Decrypt refund contents.
266
     *
267
     * @author yansongda <[email protected]>
268
     *
269
     * @param string $contents
270
     *
271
     * @return string
272
     */
273
    public static function decryptRefundContents($contents): string
274
    {
275
        return openssl_decrypt(
276
            base64_decode($contents),
277
            'AES-256-ECB',
278
            md5(self::$instance->key),
279
            OPENSSL_RAW_DATA
280
        );
281
    }
282
283
    /**
284
     * Convert array to xml.
285
     *
286
     * @author yansongda <[email protected]>
287
     *
288
     * @param array $data
289
     *
290
     * @throws InvalidArgumentException
291
     *
292
     * @return string
293
     */
294
    public static function toXml($data): string
295
    {
296
        if (!is_array($data) || count($data) <= 0) {
297
            throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!');
298
        }
299
300
        $xml = '<xml>';
301
        foreach ($data as $key => $val) {
302
            $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
303
                                       '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
304
        }
305
        $xml .= '</xml>';
306
307
        return $xml;
308
    }
309
310
    /**
311
     * Convert xml to array.
312
     *
313
     * @author yansongda <[email protected]>
314
     *
315
     * @param string $xml
316
     *
317
     * @throws InvalidArgumentException
318
     *
319
     * @return array
320
     */
321
    public static function fromXml($xml): array
322
    {
323
        if (!$xml) {
324
            throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!');
325
        }
326
327
        libxml_disable_entity_loader(true);
328
329
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
330
    }
331
332
    /**
333
     * Get service config.
334
     *
335
     * @author yansongda <[email protected]>
336
     *
337
     * @param string|null $key
338
     * @param mixed|null  $default
339
     *
340
     * @return mixed|null
341
     */
342 View Code Duplication
    public function getConfig($key = null, $default = null)
0 ignored issues
show
Duplication introduced by yansongda
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
343
    {
344
        if (is_null($key)) {
345
            return $this->config->all();
346
        }
347
348
        if ($this->config->has($key)) {
349
            return $this->config[$key];
350
        }
351
352
        return $default;
353
    }
354
355
    /**
356
     * Get app id according to param type.
357
     *
358
     * @author yansongda <[email protected]>
359
     *
360
     * @param string $type
361
     *
362
     * @return string
363
     */
364
    public static function getTypeName($type = ''): string
365
    {
366
        switch ($type) {
367
            case '':
368
                $type = 'app_id';
369
                break;
370
            case 'app':
371
                $type = 'appid';
372
                break;
373
            default:
374
                $type = $type.'_id';
375
        }
376
377
        return $type;
378
    }
379
380
    /**
381
     * Get Base Uri.
382
     *
383
     * @author yansongda <[email protected]>
384
     *
385
     * @return string
386
     */
387
    public function getBaseUri()
388
    {
389
        return $this->baseUri;
390
    }
391
392
    /**
393
     * processingApiResult.
394
     *
395
     * @author yansongda <[email protected]>
396
     *
397
     * @param       $endpoint
398
     * @param array $result
399
     *
400
     * @throws GatewayException
401
     * @throws InvalidArgumentException
402
     * @throws InvalidSignException
403
     *
404
     * @return Collection
405
     */
406
    protected static function processingApiResult($endpoint, array $result)
407
    {
408
        if (!isset($result['return_code']) || 'SUCCESS' != $result['return_code']) {
409
            throw new GatewayException(
410
                'Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg'] ?? ''),
411
                $result
412
            );
413
        }
414
415
        if (isset($result['result_code']) && 'SUCCESS' != $result['result_code']) {
416
            throw new BusinessException(
417
                'Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'],
418
                $result
419
            );
420
        }
421
422
        if ('pay/getsignkey' === $endpoint ||
423
            false !== strpos($endpoint, 'mmpaymkttransfers') ||
424
            self::generateSign($result) === $result['sign']) {
425
            return new Collection($result);
426
        }
427
428
        Events::dispatch(new Events\SignFailed('Wechat', '', $result));
429
430
        throw new InvalidSignException('Wechat Sign Verify FAILED', $result);
431
    }
432
433
    /**
434
     * setDevKey.
435
     *
436
     * @author yansongda <[email protected]>
437
     *
438
     * @throws GatewayException
439
     * @throws InvalidArgumentException
440
     * @throws InvalidSignException
441
     * @throws Exception
442
     *
443
     * @return Support
444
     */
445
    private static function setDevKey()
446
    {
447
        if (Wechat::MODE_DEV == self::$instance->mode) {
448
            $data = [
449
                'mch_id' => self::$instance->mch_id,
450
                'nonce_str' => Str::random(),
451
            ];
452
            $data['sign'] = self::generateSign($data);
453
454
            $result = self::requestApi('pay/getsignkey', $data);
455
456
            self::$instance->config->set('key', $result['sandbox_signkey']);
457
        }
458
459
        return self::$instance;
460
    }
461
462
    /**
463
     * Set Http options.
464
     *
465
     * @author yansongda <[email protected]>
466
     *
467
     * @return self
468
     */
469 View Code Duplication
    private function setHttpOptions(): self
0 ignored issues
show
Duplication introduced by yansongda
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
470
    {
471
        if ($this->config->has('http') && is_array($this->config->get('http'))) {
472
            $this->config->forget('http.base_uri');
473
            $this->httpOptions = $this->config->get('http');
474
        }
475
476
        return $this;
477
    }
478
}
479