Completed
Push — master ( 226098...506089 )
by Songda
01:38
created

Support::processingApiResult()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.5226
c 0
b 0
f 0
cc 7
nc 5
nop 2
1
<?php
2
3
namespace Yansongda\Pay\Gateways\Wechat;
4
5
use Yansongda\Pay\Events;
6
use Yansongda\Pay\Exceptions\BusinessException;
7
use Yansongda\Pay\Exceptions\GatewayException;
8
use Yansongda\Pay\Exceptions\InvalidArgumentException;
9
use Yansongda\Pay\Exceptions\InvalidSignException;
10
use Yansongda\Pay\Gateways\Wechat;
11
use Yansongda\Pay\Log;
12
use Yansongda\Supports\Collection;
13
use Yansongda\Supports\Config;
14
use Yansongda\Supports\Str;
15
use Yansongda\Supports\Traits\HasHttpRequest;
16
17
/**
18
 * @author yansongda <[email protected]>
19
 *
20
 * @property string appid
21
 * @property string app_id
22
 * @property string miniapp_id
23
 * @property string sub_appid
24
 * @property string sub_app_id
25
 * @property string sub_miniapp_id
26
 * @property string mch_id
27
 * @property string sub_mch_id
28
 * @property string key
29
 * @property string return_url
30
 * @property string cert_client
31
 * @property string cert_key
32
 * @property array log
33
 * @property array http
34
 * @property string mode
35
 */
36
class Support
37
{
38
    use HasHttpRequest;
39
40
    /**
41
     * Wechat gateway.
42
     *
43
     * @var string
44
     */
45
    protected $baseUri;
46
47
    /**
48
     * Config.
49
     *
50
     * @var Config
51
     */
52
    protected $config;
53
54
    /**
55
     * Instance.
56
     *
57
     * @var Support
58
     */
59
    private static $instance;
60
61
    /**
62
     * Bootstrap.
63
     *
64
     * @author yansongda <[email protected]>
65
     *
66
     * @param Config $config
67
     *
68
     * @throws GatewayException
69
     * @throws InvalidArgumentException
70
     * @throws InvalidSignException
71
     */
72
    private function __construct(Config $config)
73
    {
74
        $this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)];
75
        $this->config = $config;
76
77
        $this->setDevKey();
78
        $this->setHttpOptions();
79
    }
80
81
    /**
82
     * __get.
83
     *
84
     * @author yansongda <[email protected]>
85
     *
86
     * @param $key
87
     *
88
     * @return mixed|null|Config
89
     */
90
    public function __get($key)
91
    {
92
        return $this->getConfig($key);
93
    }
94
95
    /**
96
     * setDevKey.
97
     *
98
     * @author yansongda <[email protected]>
99
     *
100
     * @throws GatewayException
101
     * @throws InvalidArgumentException
102
     * @throws InvalidSignException
103
     * @throws \Exception
104
     *
105
     * @return Support
106
     */
107
    public function setDevKey()
108
    {
109
        if (self::$instance->mode == Wechat::MODE_DEV) {
110
            $data = [
111
                'mch_id' => self::$instance->mch_id,
112
                'nonce_str' => Str::random(),
113
            ];
114
            $data['sign'] = self::generateSign($data);
115
116
            $result = self::requestApi('pay/getsignkey', $data);
117
118
            self::$instance->config->set('key', $result['sandbox_signkey']);
119
        }
120
121
        return self::$instance;
122
    }
123
124
    /**
125
     * create.
126
     *
127
     * @author yansongda <[email protected]>
128
     *
129
     * @param Config $config
130
     *
131
     * @throws GatewayException
132
     * @throws InvalidArgumentException
133
     * @throws InvalidSignException
134
     *
135
     * @return Support
136
     */
137 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...
138
    {
139
        if (php_sapi_name() === 'cli' || !(self::$instance instanceof self)) {
140
            self::$instance = new self($config);
141
        }
142
143
        return self::$instance;
144
    }
145
146
    /**
147
     * getInstance.
148
     *
149
     * @author yansongda <[email protected]>
150
     *
151
     * @throws InvalidArgumentException
152
     *
153
     * @return Support
154
     */
155
    public static function getInstance()
156
    {
157
        if (is_null(self::$instance)) {
158
            throw new InvalidArgumentException('You Should [Create] First Before Using');
159
        }
160
161
        return self::$instance;
162
    }
163
164
    /**
165
     * clear.
166
     *
167
     * @author yansongda <[email protected]>
168
     *
169
     * @return void
170
     */
171
    public static function clear()
172
    {
173
        self::$instance = null;
174
    }
175
176
    /**
177
     * Request wechat api.
178
     *
179
     * @author yansongda <[email protected]>
180
     *
181
     * @param string $endpoint
182
     * @param array  $data
183
     * @param bool   $cert
184
     *
185
     * @throws GatewayException
186
     * @throws InvalidArgumentException
187
     * @throws InvalidSignException
188
     *
189
     * @return Collection
190
     */
191
    public static function requestApi($endpoint, $data, $cert = false): Collection
192
    {
193
        Events::dispatch(Events::API_REQUESTING, new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri(), $data));
194
195
        $result = self::$instance->post(
196
            $endpoint,
197
            self::toXml($data),
198
            $cert ? [
199
                'cert'    => self::$instance->cert_client,
200
                'ssl_key' => self::$instance->cert_key,
201
            ] : []
202
        );
203
        $result = is_array($result) ? $result : self::fromXml($result);
204
205
        Events::dispatch(Events::API_REQUESTED, new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri(), $result));
206
207
        return self::processingApiResult($endpoint, $result);
208
    }
209
210
    /**
211
     * Filter payload.
212
     *
213
     * @author yansongda <[email protected]>
214
     *
215
     * @param array        $payload
216
     * @param array|string $params
217
     * @param bool         $preserve_notify_url
218
     *
219
     * @throws InvalidArgumentException
220
     *
221
     * @return array
222
     */
223
    public static function filterPayload($payload, $params, $preserve_notify_url = false): array
224
    {
225
        $type = self::getTypeName($params['type'] ?? '');
226
227
        $payload = array_merge(
228
            $payload,
229
            is_array($params) ? $params : ['out_trade_no' => $params]
230
        );
231
        $payload['appid'] = self::$instance->getConfig($type, '');
232
233
        if (self::$instance->getConfig('mode', Wechat::MODE_NORMAL) === Wechat::MODE_SERVICE) {
234
            $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');
235
        }
236
237
        unset($payload['trade_type'], $payload['type']);
238
        if (!$preserve_notify_url) {
239
            unset($payload['notify_url']);
240
        }
241
242
        $payload['sign'] = self::generateSign($payload);
243
244
        return $payload;
245
    }
246
247
    /**
248
     * Generate wechat sign.
249
     *
250
     * @author yansongda <[email protected]>
251
     *
252
     * @param array $data
253
     *
254
     * @throws InvalidArgumentException
255
     *
256
     * @return string
257
     */
258
    public static function generateSign($data): string
259
    {
260
        $key = self::$instance->key;
261
262
        if (is_null($key)) {
263
            throw new InvalidArgumentException('Missing Wechat Config -- [key]');
264
        }
265
266
        ksort($data);
267
268
        $string = md5(self::getSignContent($data).'&key='.$key);
269
270
        Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]);
271
272
        return strtoupper($string);
273
    }
274
275
    /**
276
     * Generate sign content.
277
     *
278
     * @author yansongda <[email protected]>
279
     *
280
     * @param array $data
281
     *
282
     * @return string
283
     */
284
    public static function getSignContent($data): string
285
    {
286
        $buff = '';
287
288
        foreach ($data as $k => $v) {
289
            $buff .= ($k != 'sign' && $v != '' && !is_array($v)) ? $k.'='.$v.'&' : '';
290
        }
291
292
        Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]);
293
294
        return trim($buff, '&');
295
    }
296
297
    /**
298
     * Decrypt refund contents.
299
     *
300
     * @author yansongda <[email protected]>
301
     *
302
     * @param string $contents
303
     *
304
     * @return string
305
     */
306
    public static function decryptRefundContents($contents): string
307
    {
308
        return openssl_decrypt(
309
            base64_decode($contents),
310
            'AES-256-ECB',
311
            md5(self::$instance->key),
312
            OPENSSL_RAW_DATA
313
        );
314
    }
315
316
    /**
317
     * Convert array to xml.
318
     *
319
     * @author yansongda <[email protected]>
320
     *
321
     * @param array $data
322
     *
323
     * @throws InvalidArgumentException
324
     *
325
     * @return string
326
     */
327
    public static function toXml($data): string
328
    {
329
        if (!is_array($data) || count($data) <= 0) {
330
            throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!');
331
        }
332
333
        $xml = '<xml>';
334
        foreach ($data as $key => $val) {
335
            $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
336
                                       '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
337
        }
338
        $xml .= '</xml>';
339
340
        return $xml;
341
    }
342
343
    /**
344
     * Convert xml to array.
345
     *
346
     * @author yansongda <[email protected]>
347
     *
348
     * @param string $xml
349
     *
350
     * @throws InvalidArgumentException
351
     *
352
     * @return array
353
     */
354
    public static function fromXml($xml): array
355
    {
356
        if (!$xml) {
357
            throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!');
358
        }
359
360
        libxml_disable_entity_loader(true);
361
362
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
363
    }
364
365
    /**
366
     * Get service config.
367
     *
368
     * @author yansongda <[email protected]>
369
     *
370
     * @param null|string $key
371
     * @param null|mixed  $default
372
     *
373
     * @return mixed|null
374
     */
375 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...
376
    {
377
        if (is_null($key)) {
378
            return $this->config->all();
379
        }
380
381
        if ($this->config->has($key)) {
382
            return $this->config[$key];
383
        }
384
385
        return $default;
386
    }
387
388
    /**
389
     * Get app id according to param type.
390
     *
391
     * @author yansongda <[email protected]>
392
     *
393
     * @param string $type
394
     *
395
     * @return string
396
     */
397
    public static function getTypeName($type = ''): string
398
    {
399
        switch ($type) {
400
            case '':
401
                $type = 'app_id';
402
                break;
403
            case 'app':
404
                $type = 'appid';
405
                break;
406
            default:
407
                $type = $type.'_id';
408
        }
409
410
        return $type;
411
    }
412
413
    /**
414
     * Get Base Uri.
415
     *
416
     * @author yansongda <[email protected]>
417
     *
418
     * @return string
419
     */
420
    public function getBaseUri()
421
    {
422
        return $this->baseUri;
423
    }
424
425
    /**
426
     * processingApiResult.
427
     *
428
     * @author yansongda <[email protected]>
429
     *
430
     * @param       $endpoint
431
     * @param array $result
432
     *
433
     * @throws GatewayException
434
     * @throws InvalidArgumentException
435
     * @throws InvalidSignException
436
     *
437
     * @return Collection
438
     */
439
    protected static function processingApiResult($endpoint, array $result)
440
    {
441
        if (!isset($result['return_code']) || $result['return_code'] != 'SUCCESS') {
442
            throw new GatewayException(
443
                'Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg']),
444
                $result
445
            );
446
        }
447
448
        if ($endpoint === 'pay/getsignkey') {
449
            return new Collection($result);
450
        }
451
452
        if ($result['result_code'] != 'SUCCESS') {
453
            throw new BusinessException(
454
                'Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'],
455
                $result
456
            );
457
        }
458
459
        if (strpos($endpoint, 'mmpaymkttransfers') !== false ||
460
            self::generateSign($result) === $result['sign']) {
461
            return new Collection($result);
462
        }
463
464
        Events::dispatch(Events::SIGN_FAILED, new Events\SignFailed('Wechat', '', $result));
465
466
        throw new InvalidSignException('Wechat Sign Verify FAILED', $result);
467
    }
468
469
    /**
470
     * Set Http options.
471
     *
472
     * @author yansongda <[email protected]>
473
     *
474
     * @return self
475
     */
476 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...
477
    {
478
        if ($this->config->has('http') && is_array($this->config->get('http'))) {
479
            $this->config->forget('http.base_uri');
480
            $this->httpOptions = $this->config->get('http');
481
        }
482
483
        return $this;
484
    }
485
}
486