Completed
Push — master ( c3ded3...e46555 )
by Songda
13s
created

src/Gateways/Wechat/Support.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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