Browse Source

first commit

master 1.0
wuearl 2 years ago
commit
74656dba66
  1. 20
      .editorconfig
  2. 7
      .gitignore
  3. 6
      .phplint.yml
  4. 6
      .styleci.yml
  5. 37
      composer.json
  6. 47
      src/Factory.php
  7. 259
      src/Kernel/AccessToken.php
  8. 254
      src/Kernel/BaseClient.php
  9. 63
      src/Kernel/Clauses/Clause.php
  10. 22
      src/Kernel/Config.php
  11. 39
      src/Kernel/Contracts/AccessTokenInterface.php
  12. 28
      src/Kernel/Contracts/Arrayable.php
  13. 25
      src/Kernel/Contracts/EventHandlerInterface.php
  14. 35
      src/Kernel/Decorators/FinallyResult.php
  15. 34
      src/Kernel/Decorators/TerminateResult.php
  16. 113
      src/Kernel/Encryptor.php
  17. 21
      src/Kernel/Exceptions/BadRequestException.php
  18. 16
      src/Kernel/Exceptions/DecryptException.php
  19. 23
      src/Kernel/Exceptions/Exception.php
  20. 52
      src/Kernel/Exceptions/HttpException.php
  21. 21
      src/Kernel/Exceptions/InvalidArgumentException.php
  22. 21
      src/Kernel/Exceptions/InvalidConfigException.php
  23. 21
      src/Kernel/Exceptions/RuntimeException.php
  24. 21
      src/Kernel/Exceptions/UnboundServiceException.php
  25. 53
      src/Kernel/Helpers.php
  26. 116
      src/Kernel/Http/Response.php
  27. 546
      src/Kernel/Log/LogManager.php
  28. 39
      src/Kernel/Providers/ConfigServiceProvider.php
  29. 39
      src/Kernel/Providers/HttpClientServiceProvider.php
  30. 79
      src/Kernel/Providers/LogServiceProvider.php
  31. 39
      src/Kernel/Providers/RequestServiceProvider.php
  32. 144
      src/Kernel/ServiceContainer.php
  33. 85
      src/Kernel/Support/AES.php
  34. 466
      src/Kernel/Support/Arr.php
  35. 66
      src/Kernel/Support/ArrayAccessible.php
  36. 421
      src/Kernel/Support/Collection.php
  37. 133
      src/Kernel/Support/File.php
  38. 161
      src/Kernel/Support/Helpers.php
  39. 194
      src/Kernel/Support/Str.php
  40. 247
      src/Kernel/Traits/HasAttributes.php
  41. 217
      src/Kernel/Traits/HasHttpRequests.php
  42. 83
      src/Kernel/Traits/InteractsWithCache.php
  43. 241
      src/Kernel/Traits/Observable.php
  44. 89
      src/Kernel/Traits/ResponseCastable.php
  45. 38
      src/MiniProgram/Application.php
  46. 43
      src/MiniProgram/Auth/AccessToken.php
  47. 46
      src/MiniProgram/Auth/Client.php
  48. 40
      src/MiniProgram/Auth/ServiceProvider.php
  49. 48
      src/MiniProgram/Encryptor.php
  50. 68
      src/MiniProgram/KVData/Client.php
  51. 32
      src/MiniProgram/KVData/ServiceProvider.php
  52. 55
      src/MiniProgram/Message/Client.php
  53. 30
      src/MiniProgram/Message/ServiceProvider.php
  54. 91
      src/MiniProgram/Payment/Client.php
  55. 32
      src/MiniProgram/Payment/ServiceProvider.php
  56. 41
      src/MiniProgram/QRCode/Client.php
  57. 32
      src/MiniProgram/QRCode/ServiceProvider.php
  58. 69
      src/Payment/Application.php
  59. 189
      src/Payment/Kernel/BaseClient.php
  60. 18
      src/Payment/Kernel/Exceptions/InvalidSignException.php
  61. 18
      src/Payment/Kernel/Exceptions/SandboxException.php
  62. 197
      src/Payment/Notify/Handler.php
  63. 32
      src/Payment/Notify/Paid.php
  64. 46
      src/Payment/Notify/Refunded.php
  65. 49
      src/Payment/Order/Client.php
  66. 31
      src/Payment/Order/ServiceProvider.php
  67. 56
      src/Payment/Refund/Client.php
  68. 31
      src/Payment/Refund/ServiceProvider.php
  69. 59
      src/Payment/Settle/Client.php
  70. 31
      src/Payment/Settle/ServiceProvider.php
  71. 29
      src/config/bytedance.php

20
.editorconfig

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false
[*.{vue,js,scss}]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

7
.gitignore vendored

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
/.idea
/.git
/.vscode
/vendor
*.log
.env
index.php

6
.phplint.yml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
path: ./
jobs: 10
exclude:
- .github
- build
- vendor

6
.styleci.yml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
preset: symfony
enabled:
- strict
- strict_param
- ordered_use

37
composer.json

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
{
"name": "wuearl\/bytedance",
"description": "sdk for bytedance (douyin, toutiao, huoshan, xigua)",
"license": "MIT",
"authors": [
{
"name": "wuearl",
"email": "wuearl@qq.com"
}
],
"require": {
"php": ">=7.1",
"ext-openssl": "*",
"ext-json": "*",
"ext-fileinfo": "*",
"psr/simple-cache": "^1.0",
"guzzlehttp/guzzle": "^6.2",
"monolog/monolog": "^1.22 || ^2.0",
"symfony/cache": "^3.3 || ^4.3",
"pimple/pimple": "^3.0",
"symfony/psr-http-message-bridge": "^0.3 || ^1.0",
"symfony/http-foundation": "^2.7 || ^3.0 || ^4.0"
},
"autoload": {
"psr-4": {
"ByteDance\\": "src"
},
"files": [
"src/Kernel/Support/Helpers.php",
"src/Kernel/Helpers.php"
]
},
"require-dev": {
"phpunit/phpunit": "^7.5",
"overtrue/phplint": "^1.2"
}
}

47
src/Factory.php

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance;
/**
* Class Factory.
*
* @method static \ByteDance\MiniProgram\Application miniProgram(array $config)
* @method static \ByteDance\Payment\Application payment(array $config)
*/
class Factory
{
/**
* @param $name
* @param array $config
*
* @return \ByteDance\Kernel\ServiceContainer
*/
public static function make($name, array $config)
{
$namespace = Kernel\Support\Str::studly($name);
$application = "\\ByteDance\\{$namespace}\\Application";
return new $application($config);
}
/**
* Dynamically pass methods to the application.
*
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public static function __callStatic($name, $arguments)
{
return self::make($name, ...$arguments);
}
}

259
src/Kernel/AccessToken.php

@ -0,0 +1,259 @@ @@ -0,0 +1,259 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel;
use ByteDance\Kernel\Contracts\AccessTokenInterface;
use ByteDance\Kernel\Exceptions\HttpException;
use ByteDance\Kernel\Exceptions\InvalidArgumentException;
use ByteDance\Kernel\Exceptions\RuntimeException;
use ByteDance\Kernel\Traits\HasHttpRequests;
use ByteDance\Kernel\Traits\InteractsWithCache;
use Pimple\Container;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class AccessToken.
*
* @author
*/
abstract class AccessToken implements AccessTokenInterface
{
use HasHttpRequests;
use InteractsWithCache;
/**
* @var \Pimple\Container
*/
protected $app;
/**
* @var string
*/
protected $requestMethod = 'GET';
/**
* @var string
*/
protected $endpointToGetToken;
/**
* @var string
*/
protected $queryName;
/**
* @var array
*/
protected $token;
/**
* @var int
*/
protected $safeSeconds = 500;
/**
* @var string
*/
protected $tokenKey = 'access_token';
/**
* @var string
*/
protected $cachePrefix = 'otkurbiz.bytedance.kernel.access_token.';
/**
* AccessToken constructor.
*
* @param \Pimple\Container $app
*/
public function __construct(Container $app)
{
$this->app = $app;
}
/**
* @param bool $refresh
* @return array
* @throws HttpException
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function getToken(bool $refresh = false): array
{
$cacheKey = $this->getCacheKey();
$cache = $this->getCache();
if (!$refresh && $cache->has($cacheKey)) {
return $cache->get($cacheKey);
}
$token = $this->requestToken($this->getCredentials(), true);
$this->setToken($token, $token['expires_in'] ?? 7200);
return $token;
}
/**
* @param string $token
* @param int $lifetime
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\RuntimeException
*
* @return \ByteDance\Kernel\Contracts\AccessTokenInterface
*/
public function setToken(array $token, $lifetime = 7200): AccessTokenInterface
{
$this->getCache()->set($this->getCacheKey(), [
$this->tokenKey => $token[$this->tokenKey],
'expires_in' => $lifetime,
], $lifetime - $this->safeSeconds);
if (!$this->getCache()->has($this->getCacheKey())) {
throw new RuntimeException('Failed to cache access token.');
}
return $this;
}
/**
* @return AccessTokenInterface
* @throws HttpException
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function refresh(): AccessTokenInterface
{
$this->getToken(true);
return $this;
}
/**
* @param array $credentials
* @param bool $toArray
* @return array|Support\Collection|mixed|object|ResponseInterface|string
* @throws HttpException
* @throws InvalidArgumentException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function requestToken(array $credentials, $toArray = false)
{
$response = $this->sendRequest($credentials);
$result = json_decode($response->getBody()->getContents(), true);
$formatted = $this->castResponseToType($response, $this->app['config']->get('response_type'));
if (empty($result[$this->tokenKey])) {
throw new HttpException('Request access_token fail: '.json_encode($result, JSON_UNESCAPED_UNICODE), $response, $formatted);
}
return $toArray ? $result : $formatted;
}
/**
* @param RequestInterface $request
* @param array $requestOptions
* @return RequestInterface
* @throws HttpException
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface
{
parse_str($request->getUri()->getQuery(), $query);
$query = http_build_query(array_merge($this->getQuery(), $query));
return $request->withUri($request->getUri()->withQuery($query));
}
/**
* @param array $requestOptions
* @return array
* @throws HttpException
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function applyToOptions(array $requestOptions = []): array
{
if(isset($requestOptions['json'])){
$requestOptions['json'] = array_merge($this->getQuery(), $requestOptions['json']);
}
return $requestOptions;
}
/**
* Send http request.
*
* @param array $credentials
*
* @return ResponseInterface
* @throws \GuzzleHttp\Exception\GuzzleException
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*/
protected function sendRequest(array $credentials): ResponseInterface
{
$options = [
('GET' === $this->requestMethod) ? 'query' : 'json' => $credentials,
];
return $this->setHttpClient($this->app['http_client'])->request($this->getEndpoint(), $this->requestMethod, $options);
}
/**
* @return string
*/
protected function getCacheKey()
{
return $this->cachePrefix.md5(json_encode($this->getCredentials()));
}
/**
* @return array
* @throws HttpException
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function getQuery(): array
{
return [$this->queryName ?? $this->tokenKey => $this->getToken()[$this->tokenKey]];
}
/**
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*
* @return string
*/
public function getEndpoint(): string
{
if (empty($this->endpointToGetToken)) {
throw new InvalidArgumentException('No endpoint for access token request.');
}
return $this->endpointToGetToken;
}
/**
* @return string
*/
public function getTokenKey()
{
return $this->tokenKey;
}
/**
* Credential for get token.
*
* @return array
*/
abstract protected function getCredentials(): array;
}

254
src/Kernel/BaseClient.php

@ -0,0 +1,254 @@ @@ -0,0 +1,254 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Middleware;
use ByteDance\Kernel\Contracts\AccessTokenInterface;
use ByteDance\Kernel\Http\Response;
use ByteDance\Kernel\Traits\HasHttpRequests;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class BaseClient.
*
* @author overtrue <i@overtrue.me>
*/
class BaseClient
{
use HasHttpRequests { request as performRequest; }
/**
* @var \ByteDance\Kernel\ServiceContainer
*/
protected $app;
/**
* @var \ByteDance\Kernel\Contracts\AccessTokenInterface
*/
protected $accessToken;
/**
* @var
*/
protected $baseUri;
/**
* BaseClient constructor.
*
* @param \ByteDance\Kernel\ServiceContainer $app
* @param \ByteDance\Kernel\Contracts\AccessTokenInterface|null $accessToken
*/
public function __construct(ServiceContainer $app, AccessTokenInterface $accessToken = null)
{
$this->app = $app;
$this->accessToken = $accessToken ?? $this->app['access_token'];
}
/**
* GET request.
* @param string $url
* @param array $query
* @return array|Support\Collection|object|ResponseInterface|string
* @throws Exceptions\InvalidArgumentException
* @throws Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpGet(string $url, array $query = [])
{
return $this->request($url, 'GET', ['query' => $query]);
}
/**
* POST request.
* @param string $url
* @param array $data
* @return array|Support\Collection|object|ResponseInterface|string
* @throws Exceptions\InvalidArgumentException
* @throws Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpPost(string $url, array $data = [])
{
return $this->request($url, 'POST', ['form_params' => $data]);
}
/**
* JSON request.
* @param string $url
* @param array $data
* @param array $query
* @return array|Support\Collection|object|ResponseInterface|string
* @throws Exceptions\InvalidArgumentException
* @throws Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpPostJson(string $url, array $data = [], array $query = [])
{
return $this->request($url, 'POST', ['query' => $query, 'json' => $data],true);
}
/**
* Upload file.
* @param string $url
* @param array $files
* @param array $form
* @param array $query
* @return array|Support\Collection|object|ResponseInterface|string
* @throws Exceptions\InvalidArgumentException
* @throws Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpUpload(string $url, array $files = [], array $form = [], array $query = [])
{
$multipart = [];
foreach ($files as $name => $path) {
$multipart[] = [
'name' => $name,
'contents' => fopen($path, 'r'),
];
}
foreach ($form as $name => $contents) {
$multipart[] = compact('name', 'contents');
}
return $this->request($url, 'POST', ['query' => $query, 'multipart' => $multipart, 'connect_timeout' => 30, 'timeout' => 30, 'read_timeout' => 30]);
}
/**
* @return AccessTokenInterface
*/
public function getAccessToken(): AccessTokenInterface
{
return $this->accessToken;
}
/**
* @param \ByteDance\Kernel\Contracts\AccessTokenInterface $accessToken
*
* @return $this
*/
public function setAccessToken(AccessTokenInterface $accessToken)
{
$this->accessToken = $accessToken;
return $this;
}
/**
* @param string $url
* @param string $method
* @param array $options
* @param bool $returnRaw
*
* @return \Psr\Http\Message\ResponseInterface|\ByteDance\Kernel\Support\Collection|array|object|string
* @throws \GuzzleHttp\Exception\GuzzleException
*
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws Exceptions\InvalidArgumentException
*/
public function request(string $url, string $method = 'GET', array $options = [], $returnRaw = false)
{
if (empty($this->middlewares)) {
$this->registerHttpMiddlewares();
if ($this->accessToken) {
$options = $this->accessToken->applyToOptions($options);
}
}
$response = $this->performRequest($url, $method, $options);
return $returnRaw ? $response : $this->castResponseToType($response, $this->app->config->get('response_type'));
}
/**
* @param string $url
* @param string $method
* @param array $options
* @return Response
* @throws Exceptions\InvalidArgumentException
* @throws Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function requestRaw(string $url, string $method = 'GET', array $options = [])
{
return Response::buildFromPsrResponse($this->request($url, $method, $options, true));
}
/**
* Register Guzzle middlewares.
*/
protected function registerHttpMiddlewares()
{
// retry
$this->pushMiddleware($this->retryMiddleware(), 'retry');
// access token
$this->pushMiddleware($this->accessTokenMiddleware(), 'access_token');
// log
$this->pushMiddleware($this->logMiddleware(), 'log');
}
/**
* Attache access token to request query.
*
* @return \Closure
*/
protected function accessTokenMiddleware()
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
if ($this->accessToken) {
$request = $this->accessToken->applyToRequest($request, $options);
}
return $handler($request, $options);
};
};
}
/**
* Log the request.
*
* @return \Closure
*/
protected function logMiddleware()
{
$formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG);
return Middleware::log($this->app['logger'], $formatter);
}
/**
* Return retry middleware.
*
* @return \Closure
*/
protected function retryMiddleware()
{
return Middleware::retry(function (
$retries,
RequestInterface $request,
ResponseInterface $response = null
) {
// Limit the number of retries to 2
if ($retries < $this->app->config->get('http.max_retries', 1) && $response && $body = $response->getBody()) {
// Retry on server errors
$response = json_decode($body, true);
if (!empty($response['errcode']) && in_array(abs($response['errcode']), [40001, 40002, 40014, 42001], true)) {
$this->accessToken->refresh();
$this->app['logger']->debug('Retrying with refreshed access token.');
return true;
}
}
return false;
}, function () {
return abs($this->app->config->get('http.retry_delay', 500));
});
}
}

63
src/Kernel/Clauses/Clause.php

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Clauses;
/**
* Class Clause.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class Clause
{
/**
* @var array
*/
protected $clauses = [
'where' => [],
];
/**
* @param mixed ...$args
*
* @return $this
*/
public function where(...$args)
{
array_push($this->clauses['where'], $args);
return $this;
}
/**
* @param mixed $payload
*
* @return bool
*/
public function intercepted($payload)
{
return (bool) $this->interceptWhereClause($payload);
}
/**
* @param mixed $payload
*
* @return bool
*/
protected function interceptWhereClause($payload)
{
foreach ($this->clauses['where'] as $item) {
list($key, $value) = $item;
if (isset($payload[$key]) && $payload[$key] !== $value) {
return true;
}
}
}
}

22
src/Kernel/Config.php

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel;
use ByteDance\Kernel\Support\Collection;
/**
* Class Config.
*
* @author overtrue <i@overtrue.me>
*/
class Config extends Collection
{
}

39
src/Kernel/Contracts/AccessTokenInterface.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Contracts;
use Psr\Http\Message\RequestInterface;
/**
* Interface AuthorizerAccessToken.
*
* @author overtrue <i@overtrue.me>
*/
interface AccessTokenInterface
{
/**
* @return array
*/
public function getToken(): array;
/**
* @return \ByteDance\Kernel\Contracts\AccessTokenInterface
*/
public function refresh(): self;
/**
* @param \Psr\Http\Message\RequestInterface $request
* @param array $requestOptions
*
* @return \Psr\Http\Message\RequestInterface
*/
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface;
}

28
src/Kernel/Contracts/Arrayable.php

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Contracts;
use ArrayAccess;
/**
* Interface Arrayable.
*
* @author overtrue <i@overtrue.me>
*/
interface Arrayable extends ArrayAccess
{
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray();
}

25
src/Kernel/Contracts/EventHandlerInterface.php

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Contracts;
/**
* Interface EventHandlerInterface.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
interface EventHandlerInterface
{
/**
* @param mixed $payload
*/
public function handle($payload = null);
}

35
src/Kernel/Decorators/FinallyResult.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Decorators;
/**
* Class FinallyResult.
*
* @author overtrue <i@overtrue.me>
*/
class FinallyResult
{
/**
* @var mixed
*/
public $content;
/**
* FinallyResult constructor.
*
* @param mixed $content
*/
public function __construct($content)
{
$this->content = $content;
}
}

34
src/Kernel/Decorators/TerminateResult.php

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Decorators;
/**
* Class TerminateResult.
*
* @author overtrue <i@overtrue.me>
*/
class TerminateResult
{
/**
* @var mixed
*/
public $content;
/**
* FinallyResult constructor.
*
* @param mixed $content
*/
public function __construct($content)
{
$this->content = $content;
}
}

113
src/Kernel/Encryptor.php

@ -0,0 +1,113 @@ @@ -0,0 +1,113 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel;
use ByteDance\Kernel\Exceptions\RuntimeException;
use ByteDance\Kernel\Support\AES;
use Throwable;
use function ByteDance\Kernel\Support\str_random;
/**
* Class Encryptor.
*
* @author overtrue <i@overtrue.me>
*/
class Encryptor
{
const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed
const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed
const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey
const ERROR_INVALID_APP_ID = -40005; // Check AppID failed
const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed
const ERROR_DECRYPT_AES = -40007; // AES decryption failed
const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed
const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed
const ILLEGAL_BUFFER = -41003; // Illegal buffer
/**
* @var string
*/
protected $aesKey;
/**
* Block size.
*
* @var int
*/
protected $blockSize = 32;
/**
* Constructor.
*
* @param string $appId
* @param string|null $token
* @param string|null $aesKey
*/
public function __construct(string $aesKey = null)
{
$this->aesKey = base64_decode($aesKey.'=', true);
}
/**
* Get SHA1.
*
* @return string
*
* @throws self
*/
public function signature(): string
{
$array = func_get_args();
sort($array, SORT_STRING);
return sha1(implode($array));
}
/**
* PKCS#7 pad.
*
* @param string $text
* @param int $blockSize
*
* @return string
*
* @throws \ByteDance\Kernel\Exceptions\RuntimeException
*/
public function pkcs7Pad(string $text, int $blockSize): string
{
if ($blockSize > 256) {
throw new RuntimeException('$blockSize may not be more than 256');
}
$padding = $blockSize - (strlen($text) % $blockSize);
$pattern = chr($padding);
return $text.str_repeat($pattern, $padding);
}
/**
* PKCS#7 unpad.
*
* @param string $text
*
* @return string
*/
public function pkcs7Unpad(string $text): string
{
$pad = ord(substr($text, -1));
if ($pad < 1 || $pad > $this->blockSize) {
$pad = 0;
}
return substr($text, 0, (strlen($text) - $pad));
}
}

21
src/Kernel/Exceptions/BadRequestException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
/**
* Class BadRequestException.
*
* @author overtrue <i@overtrue.me>
*/
class BadRequestException extends Exception
{
}

16
src/Kernel/Exceptions/DecryptException.php

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
class DecryptException extends Exception
{
}

23
src/Kernel/Exceptions/Exception.php

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
use Exception as BaseException;
/**
* Class Exception.
*
* @author overtrue <i@overtrue.me>
*/
class Exception extends BaseException
{
}

52
src/Kernel/Exceptions/HttpException.php

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
use Psr\Http\Message\ResponseInterface;
/**
* Class HttpException.
*
* @author overtrue <i@overtrue.me>
*/
class HttpException extends Exception
{
/**
* @var \Psr\Http\Message\ResponseInterface|null
*/
public $response;
/**
* @var \Psr\Http\Message\ResponseInterface|\ByteDance\Kernel\Support\Collection|array|object|string
*/
public $formattedResponse;
/**
* HttpException constructor.
*
* @param string $message
* @param \Psr\Http\Message\ResponseInterface|null $response
* @param null $formattedResponse
* @param int|null $code
*/
public function __construct($message, ResponseInterface $response = null, $formattedResponse = null, $code = null)
{
parent::__construct($message, $code);
$this->response = $response;
$this->formattedResponse = $formattedResponse;
if ($response) {
$response->getBody()->rewind();
}
}
}

21
src/Kernel/Exceptions/InvalidArgumentException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
/**
* Class InvalidArgumentException.
*
* @author overtrue <i@overtrue.me>
*/
class InvalidArgumentException extends Exception
{
}

21
src/Kernel/Exceptions/InvalidConfigException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
/**
* Class InvalidConfigException.
*
* @author overtrue <i@overtrue.me>
*/
class InvalidConfigException extends Exception
{
}

21
src/Kernel/Exceptions/RuntimeException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
/**
* Class RuntimeException.
*
* @author overtrue <i@overtrue.me>
*/
class RuntimeException extends Exception
{
}

21
src/Kernel/Exceptions/UnboundServiceException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Exceptions;
/**
* Class InvalidConfigException.
*
* @author overtrue <i@overtrue.me>
*/
class UnboundServiceException extends Exception
{
}

53
src/Kernel/Helpers.php

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel;
use ByteDance\Kernel\Contracts\Arrayable;
use ByteDance\Kernel\Exceptions\RuntimeException;
use ByteDance\Kernel\Support\Arr;
use ByteDance\Kernel\Support\Collection;
function data_get($data, $key, $default = null)
{
switch (true) {
case is_array($data):
return Arr::get($data, $key, $default);
case $data instanceof Collection:
return $data->get($key, $default);
case $data instanceof Arrayable:
return Arr::get($data->toArray(), $key, $default);
case $data instanceof \ArrayIterator:
return $data->getArrayCopy()[$key] ?? $default;
case $data instanceof \ArrayAccess:
return $data[$key] ?? $default;
case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator:
return $data->getIterator()->getArrayCopy()[$key] ?? $default;
default:
throw new RuntimeException(sprintf('Can\'t access data with key "%s"', $key));
}
}
function data_to_array($data)
{
switch (true) {
case is_array($data):
return $data;
case $data instanceof Collection:
return $data->all();
case $data instanceof Arrayable:
return $data->toArray();
case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator:
return $data->getIterator()->getArrayCopy();
case $data instanceof \ArrayIterator:
return $data->getArrayCopy();
default:
throw new RuntimeException(sprintf('Can\'t transform data to array'));
}
}

116
src/Kernel/Http/Response.php

@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Http;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use ByteDance\Kernel\Support\Collection;
use Psr\Http\Message\ResponseInterface;
/**
* Class Response.
*
* @author overtrue <i@overtrue.me>
*/
class Response extends GuzzleResponse
{
/**
* @return string
*/
public function getBodyContents()
{
$this->getBody()->rewind();
$contents = $this->getBody()->getContents();
$this->getBody()->rewind();
return $contents;
}
/**
* @param \Psr\Http\Message\ResponseInterface $response
*
* @return \ByteDance\Kernel\Http\Response
*/
public static function buildFromPsrResponse(ResponseInterface $response)
{
return new static(
$response->getStatusCode(),
$response->getHeaders(),
$response->getBody(),
$response->getProtocolVersion(),
$response->getReasonPhrase()
);
}
/**
* Build to json.
*
* @return string
*/
public function toJson()
{
return json_encode($this->toArray());
}
/**
* Build to array.
*
* @return array
*/
public function toArray()
{
$content = $this->removeControlCharacters($this->getBodyContents());
$array = json_decode($content, true, 512, JSON_BIGINT_AS_STRING);
if (JSON_ERROR_NONE === json_last_error()) {
return (array) $array;
}
return [];
}
/**
* Get collection data.
*
* @return \ByteDance\Kernel\Support\Collection
*/
public function toCollection()
{
return new Collection($this->toArray());
}
/**
* @return object
*/
public function toObject()
{
return json_decode($this->toJson());
}
/**
* @return bool|string
*/
public function __toString()
{
return $this->getBodyContents();
}
/**
* @param string $content
*
* @return string
*/
protected function removeControlCharacters(string $content)
{
return \preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $content);
}
}

546
src/Kernel/Log/LogManager.php

@ -0,0 +1,546 @@ @@ -0,0 +1,546 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Log;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Logger as Monolog;
use ByteDance\Kernel\ServiceContainer;
use Psr\Log\LoggerInterface;
/**
* Class LogManager.
*
* @author overtrue <i@overtrue.me>
*/
class LogManager implements LoggerInterface
{
/**
* @var \ByteDance\Kernel\ServiceContainer
*/
protected $app;
/**
* The array of resolved channels.
*
* @var array
*/
protected $channels = [];
/**
* The registered custom driver creators.
*
* @var array
*/
protected $customCreators = [];
/**
* The Log levels.
*
* @var array
*/
protected $levels = [
'debug' => Monolog::DEBUG,
'info' => Monolog::INFO,
'notice' => Monolog::NOTICE,
'warning' => Monolog::WARNING,
'error' => Monolog::ERROR,
'critical' => Monolog::CRITICAL,
'alert' => Monolog::ALERT,
'emergency' => Monolog::EMERGENCY,
];
/**
* LogManager constructor.
*
* @param \ByteDance\Kernel\ServiceContainer $app
*/
public function __construct(ServiceContainer $app)
{
$this->app = $app;
}
/**
* Create a new, on-demand aggregate logger instance.
*
* @param array $channels
* @param string|null $channel
*
* @return \Psr\Log\LoggerInterface
*/
public function stack(array $channels, $channel = null)
{
return $this->createStackDriver(compact('channels', 'channel'));
}
/**
* Get a log channel instance.
*
* @param string|null $channel
*
* @return mixed
*/
public function channel($channel = null)
{
return $this->get($channel);
}
/**
* Get a log driver instance.
*
* @param string|null $driver
*
* @return mixed
*/
public function driver($driver = null)
{
return $this->get($driver ?? $this->getDefaultDriver());
}
/**
* Attempt to get the log from the local cache.
*
* @param string $name
*
* @return \Psr\Log\LoggerInterface
*/
protected function get($name)
{
try {
return $this->channels[$name] ?? ($this->channels[$name] = $this->resolve($name));
} catch (\Throwable $e) {
$logger = $this->createEmergencyLogger();
$logger->emergency('Unable to create configured logger. Using emergency logger.', [
'exception' => $e,
]);
return $logger;
}
}
/**
* Resolve the given log instance by name.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return \Psr\Log\LoggerInterface
*/
protected function resolve($name)
{
$config = $this->app['config']->get(\sprintf('log.channels.%s', $name));
if (is_null($config)) {
throw new \InvalidArgumentException(\sprintf('Log [%s] is not defined.', $name));
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($config);
}
throw new \InvalidArgumentException(\sprintf('Driver [%s] is not supported.', $config['driver']));
}
/**
* Create an emergency log handler to avoid white screens of death.
*
* @return \Monolog\Logger
* @throws \Exception
*/
protected function createEmergencyLogger()
{
return new Monolog('EasyWeChat', $this->prepareHandlers([new StreamHandler(
\sys_get_temp_dir().'/easywechat/easywechat.log', $this->level(['level' => 'debug'])
)]));
}
/**
* Call a custom driver creator.
*
* @param array $config
*
* @return mixed
*/
protected function callCustomCreator(array $config)
{
return $this->customCreators[$config['driver']]($this->app, $config);
}
/**
* Create an aggregate log driver instance.
*
* @param array $config
*
* @return \Monolog\Logger
*/
protected function createStackDriver(array $config)
{
$handlers = [];
foreach ($config['channels'] ?? [] as $channel) {
$handlers = \array_merge($handlers, $this->channel($channel)->getHandlers());
}
return new Monolog($this->parseChannel($config), $handlers);
}
/**
* Create an instance of the single file log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
* @throws \Exception
*/
protected function createSingleDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(
new StreamHandler($config['path'], $this->level($config))
),
]);
}
/**
* Create an instance of the daily file log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createDailyDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new RotatingFileHandler(
$config['path'], $config['days'] ?? 7, $this->level($config)
)),
]);
}
/**
* Create an instance of the Slack log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createSlackDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new SlackWebhookHandler(
$config['url'],
$config['channel'] ?? null,
$config['username'] ?? 'EasyWeChat',
$config['attachment'] ?? true,
$config['emoji'] ?? ':boom:',
$config['short'] ?? false,
$config['context'] ?? true,
$this->level($config)
)),
]);
}
/**
* Create an instance of the syslog log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createSyslogDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new SyslogHandler(
'EasyWeChat', $config['facility'] ?? LOG_USER, $this->level($config))
),
]);
}
/**
* Create an instance of the "error log" log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createErrorlogDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new ErrorLogHandler(
$config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM, $this->level($config))
),
]);
}
/**
* Prepare the handlers for usage by Monolog.
*
* @param array $handlers
*
* @return array
*/
protected function prepareHandlers(array $handlers)
{
foreach ($handlers as $key => $handler) {
$handlers[$key] = $this->prepareHandler($handler);
}
return $handlers;
}
/**
* Prepare the handler for usage by Monolog.
*
* @param \Monolog\Handler\HandlerInterface $handler
*
* @return \Monolog\Handler\HandlerInterface
*/
protected function prepareHandler(HandlerInterface $handler)
{
return $handler->setFormatter($this->formatter());
}
/**
* Get a Monolog formatter instance.
*
* @return \Monolog\Formatter\FormatterInterface
*/
protected function formatter()
{
$formatter = new LineFormatter(null, null, true, true);
$formatter->includeStacktraces();
return $formatter;
}
/**
* Extract the log channel from the given configuration.
*
* @param array $config
*
* @return string
*/
protected function parseChannel(array $config)
{
return $config['name'] ?? null;
}
/**
* Parse the string level into a Monolog constant.
*
* @param array $config
*
* @throws \InvalidArgumentException
*
* @return int
*/
protected function level(array $config)
{
$level = $config['level'] ?? 'debug';
if (isset($this->levels[$level])) {
return $this->levels[$level];
}
throw new \InvalidArgumentException('Invalid log level.');
}
/**
* Get the default log driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['log.default'];
}
/**
* Set the default log driver name.
*
* @param string $name
*/
public function setDefaultDriver($name)
{
$this->app['config']['log.default'] = $name;
}
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param \Closure $callback
*
* @return $this
*/
public function extend($driver, \Closure $callback)
{
$this->customCreators[$driver] = $callback->bindTo($this, $this);
return $this;
}
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function emergency($message, array $context = [])
{
return $this->driver()->emergency($message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function alert($message, array $context = [])
{
return $this->driver()->alert($message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function critical($message, array $context = [])
{
return $this->driver()->critical($message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function error($message, array $context = [])
{
return $this->driver()->error($message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function warning($message, array $context = [])
{
return $this->driver()->warning($message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function notice($message, array $context = [])
{
return $this->driver()->notice($message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function info($message, array $context = [])
{
return $this->driver()->info($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return mixed
*/
public function debug($message, array $context = [])
{
return $this->driver()->debug($message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return mixed
*/
public function log($level, $message, array $context = [])
{
return $this->driver()->log($level, $message, $context);
}
/**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
*
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
}

39
src/Kernel/Providers/ConfigServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Providers;
use ByteDance\Kernel\Config;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ConfigServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ConfigServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['config'] = function ($app) {
return new Config($app->getConfig());
};
}
}

39
src/Kernel/Providers/HttpClientServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Providers;
use GuzzleHttp\Client;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class HttpClientServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class HttpClientServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['http_client'] = function ($app) {
return new Client($app['config']->get('http', []));
};
}
}

79
src/Kernel/Providers/LogServiceProvider.php

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Providers;
use ByteDance\Kernel\Log\LogManager;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class LoggingServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class LogServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['logger'] = $pimple['log'] = function ($app) {
$config = $this->formatLogConfig($app);
if (!empty($config)) {
$app->rebind('config', $app['config']->merge($config));
}
return new LogManager($app);
};
}
public function formatLogConfig($app)
{
if (!empty($app['config']->get('log.channels'))) {
return $app['config']->get('log');
}
if (empty($app['config']->get('log'))) {
return [
'log' => [
'default' => 'errorlog',
'channels' => [
'errorlog' => [
'driver' => 'errorlog',
'level' => 'debug',
],
],
],
];
}
return [
'log' => [
'default' => 'single',
'channels' => [
'single' => [
'driver' => 'single',
'path' => $app['config']->get('log.file') ?: \sys_get_temp_dir().'/logs/easywechat.log',
'level' => $app['config']->get('log.level', 'debug'),
],
],
],
];
}
}

39
src/Kernel/Providers/RequestServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Providers;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Class RequestServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class RequestServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['request'] = function () {
return Request::createFromGlobals();
};
}
}

144
src/Kernel/ServiceContainer.php

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel;
use ByteDance\Kernel\Providers\ConfigServiceProvider;
use ByteDance\Kernel\Providers\HttpClientServiceProvider;
use ByteDance\Kernel\Providers\LogServiceProvider;
use ByteDance\Kernel\Providers\RequestServiceProvider;
use Pimple\Container;
/**
* Class ServiceContainer.
*
* @author overtrue <i@overtrue.me>
*
* @property \ByteDance\Kernel\Config $config
* @property \Symfony\Component\HttpFoundation\Request $request
* @property \GuzzleHttp\Client $http_client
* @property \Monolog\Logger $logger
*/
class ServiceContainer extends Container
{
/**
* @var string
*/
protected $id;
/**
* @var array
*/
protected $providers = [];
/**
* @var array
*/
protected $defaultConfig = [];
/**
* @var array
*/
protected $userConfig = [];
/**
* Constructor.
*
* @param array $config
* @param array $prepends
* @param string|null $id
*/
public function __construct(array $config = [], array $prepends = [], string $id = null)
{
$this->registerProviders($this->getProviders());
parent::__construct($prepends);
$this->userConfig = $config;
$this->id = $id;
}
/**
* @return string
*/
public function getId()
{
return $this->id ?? $this->id = md5(json_encode($this->userConfig));
}
/**
* @return array
*/
public function getConfig()
{
$base = [
// http://docs.guzzlephp.org/en/stable/request-options.html
'http' => [
'timeout' => 30.0,
'base_uri' => 'https://developer.toutiao.com/',
],
];
return array_replace_recursive($base, $this->defaultConfig, $this->userConfig);
}
/**
* Return all providers.
*
* @return array
*/
public function getProviders()
{
return array_merge([
ConfigServiceProvider::class,
LogServiceProvider::class,
RequestServiceProvider::class,
HttpClientServiceProvider::class,
], $this->providers);
}
/**
* @param string $id
* @param mixed $value
*/
public function rebind($id, $value)
{
$this->offsetUnset($id);
$this->offsetSet($id, $value);
}
/**
* Magic get access.
*
* @param string $id
*
* @return mixed
*/
public function __get($id)
{
return $this->offsetGet($id);
}
/**
* Magic set access.
*
* @param string $id
* @param mixed $value
*/
public function __set($id, $value)
{
$this->offsetSet($id, $value);
}
/**
* @param array $providers
*/
public function registerProviders(array $providers)
{
foreach ($providers as $provider) {
parent::register(new $provider());
}
}
}

85
src/Kernel/Support/AES.php

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
/**
* Class AES.
*
* @author overtrue <i@overtrue.me>
*/
class AES
{
/**
* @param string $text
* @param string $key
* @param string $iv
* @param int $option
*
* @return string
*/
public static function encrypt(string $text, string $key, string $iv, int $option = OPENSSL_RAW_DATA): string
{
self::validateKey($key);
self::validateIv($iv);
return openssl_encrypt($text, self::getMode($key), $key, $option, $iv);
}
/**
* @param string $cipherText
* @param string $key
* @param string $iv
* @param int $option
* @param string|null $method
*
* @return string
*/
public static function decrypt(string $cipherText, string $key, string $iv, int $option = OPENSSL_RAW_DATA, $method = null): string
{
self::validateKey($key);
self::validateIv($iv);
return openssl_decrypt($cipherText, $method ?: self::getMode($key), $key, $option, $iv);
}
/**
* @param string $key
*
* @return string
*/
public static function getMode($key)
{
return 'aes-'.(8 * strlen($key)).'-cbc';
}
/**
* @param string $key
*/
public static function validateKey(string $key)
{
if (!in_array(strlen($key), [16, 24, 32], true)) {
throw new \InvalidArgumentException(sprintf('Key length must be 16, 24, or 32 bytes; got key len (%s).', strlen($key)));
}
}
/**
* @param string $iv
*
* @throws \InvalidArgumentException
*/
public static function validateIv(string $iv)
{
if (!empty($iv) && 16 !== strlen($iv)) {
throw new \InvalidArgumentException('IV length must be 16 bytes.');
}
}
}

466
src/Kernel/Support/Arr.php

@ -0,0 +1,466 @@ @@ -0,0 +1,466 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
/**
* Array helper from Illuminate\Support\Arr.
*/
class Arr
{
/**
* Add an element to an array using "dot" notation if it doesn't exist.
*
* @param array $array
* @param string $key
* @param mixed $value
*
* @return array
*/
public static function add(array $array, $key, $value)
{
if (is_null(static::get($array, $key))) {
static::set($array, $key, $value);
}
return $array;
}
/**
* Cross join the given arrays, returning all possible permutations.
*
* @param array ...$arrays
*
* @return array
*/
public static function crossJoin(...$arrays)
{
$results = [[]];
foreach ($arrays as $index => $array) {
$append = [];
foreach ($results as $product) {
foreach ($array as $item) {
$product[$index] = $item;
$append[] = $product;
}
}
$results = $append;
}
return $results;
}
/**
* Divide an array into two arrays. One with keys and the other with values.
*
* @param array $array
*
* @return array
*/
public static function divide(array $array)
{
return [array_keys($array), array_values($array)];
}
/**
* Flatten a multi-dimensional associative array with dots.
*
* @param array $array
* @param string $prepend
*
* @return array
*/
public static function dot(array $array, $prepend = '')
{
$results = [];
foreach ($array as $key => $value) {
if (is_array($value) && !empty($value)) {
$results = array_merge($results, static::dot($value, $prepend.$key.'.'));
} else {
$results[$prepend.$key] = $value;
}
}
return $results;
}
/**
* Get all of the given array except for a specified array of items.
*
* @param array $array
* @param array|string $keys
*
* @return array
*/
public static function except(array $array, $keys)
{
static::forget($array, $keys);
return $array;
}
/**
* Determine if the given key exists in the provided array.
*
* @param \ArrayAccess|array $array
* @param string|int $key
*
* @return bool
*/
public static function exists(array $array, $key)
{
return array_key_exists($key, $array);
}
/**
* Return the first element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
*
* @return mixed
*/
public static function first(array $array, callable $callback = null, $default = null)
{
if (is_null($callback)) {
if (empty($array)) {
return $default;
}
foreach ($array as $item) {
return $item;
}
}
foreach ($array as $key => $value) {
if (call_user_func($callback, $value, $key)) {
return $value;
}
}
return $default;
}
/**
* Return the last element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
*
* @return mixed
*/
public static function last(array $array, callable $callback = null, $default = null)
{
if (is_null($callback)) {
return empty($array) ? $default : end($array);
}
return static::first(array_reverse($array, true), $callback, $default);
}
/**
* Flatten a multi-dimensional array into a single level.
*
* @param array $array
* @param int $depth
*
* @return array
*/
public static function flatten(array $array, $depth = INF)
{
return array_reduce($array, function ($result, $item) use ($depth) {
$item = $item instanceof Collection ? $item->all() : $item;
if (!is_array($item)) {
return array_merge($result, [$item]);
} elseif (1 === $depth) {
return array_merge($result, array_values($item));
}
return array_merge($result, static::flatten($item, $depth - 1));
}, []);
}
/**
* Remove one or many array items from a given array using "dot" notation.
*
* @param array $array
* @param array|string $keys
*/
public static function forget(array &$array, $keys)
{
$original = &$array;
$keys = (array) $keys;
if (0 === count($keys)) {
return;
}
foreach ($keys as $key) {
// if the exact key exists in the top-level, remove it
if (static::exists($array, $key)) {
unset($array[$key]);
continue;
}
$parts = explode('.', $key);
// clean up before each pass
$array = &$original;
while (count($parts) > 1) {
$part = array_shift($parts);
if (isset($array[$part]) && is_array($array[$part])) {
$array = &$array[$part];
} else {
continue 2;
}
}
unset($array[array_shift($parts)]);
}
}
/**
* Get an item from an array using "dot" notation.
*
* @param \ArrayAccess|array $array
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public static function get(array $array, $key, $default = null)
{
if (is_null($key)) {
return $array;
}
if (static::exists($array, $key)) {
return $array[$key];
}
foreach (explode('.', $key) as $segment) {
if (static::exists($array, $segment)) {
$array = $array[$segment];
} else {
return $default;
}
}
return $array;
}
/**
* Check if an item or items exist in an array using "dot" notation.
*
* @param \ArrayAccess|array $array
* @param string|array $keys
*
* @return bool
*/
public static function has(array $array, $keys)
{
if (is_null($keys)) {
return false;
}
$keys = (array) $keys;
if (empty($array)) {
return false;
}
if ($keys === []) {
return false;
}
foreach ($keys as $key) {
$subKeyArray = $array;
if (static::exists($array, $key)) {
continue;
}
foreach (explode('.', $key) as $segment) {
if (static::exists($subKeyArray, $segment)) {
$subKeyArray = $subKeyArray[$segment];
} else {
return false;
}
}
}
return true;
}
/**
* Determines if an array is associative.
*
* An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
*
* @param array $array
*
* @return bool
*/
public static function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
/**
* Get a subset of the items from the given array.
*
* @param array $array
* @param array|string $keys
*
* @return array
*/
public static function only(array $array, $keys)
{
return array_intersect_key($array, array_flip((array) $keys));
}
/**
* Push an item onto the beginning of an array.
*
* @param array $array
* @param mixed $value
* @param mixed $key
*
* @return array
*/
public static function prepend(array $array, $value, $key = null)
{
if (is_null($key)) {
array_unshift($array, $value);
} else {
$array = [$key => $value] + $array;
}
return $array;
}
/**
* Get a value from the array, and remove it.
*
* @param array $array
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public static function pull(array &$array, $key, $default = null)
{
$value = static::get($array, $key, $default);
static::forget($array, $key);
return $value;
}
/**
* Get a 1 value from an array.
*
* @param array $array
* @param int|null $amount
*
* @throws \InvalidArgumentException
*
* @return mixed
*/
public static function random(array $array, int $amount = null)
{
if (is_null($amount)) {
return $array[array_rand($array)];
}
$keys = array_rand($array, $amount);
$results = [];
foreach ((array) $keys as $key) {
$results[] = $array[$key];
}
return $results;
}
/**
* Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string $key
* @param mixed $value
*
* @return array
*/
public static function set(array &$array, string $key, $value)
{
$keys = explode('.', $key);
while (count($keys) > 1) {
$key = array_shift($keys);
// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
/**
* Filter the array using the given callback.
*
* @param array $array
* @param callable $callback
*
* @return array
*/
public static function where(array $array, callable $callback)
{
return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
}
/**
* If the given value is not an array, wrap it in one.
*
* @param mixed $value
*
* @return array
*/
public static function wrap($value)
{
return !is_array($value) ? [$value] : $value;
}
}

66
src/Kernel/Support/ArrayAccessible.php

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use ByteDance\Kernel\Contracts\Arrayable;
/**
* Class ArrayAccessible.
*
* @author overtrue <i@overtrue.me>
*/
class ArrayAccessible implements ArrayAccess, IteratorAggregate, Arrayable
{
private $array;
public function __construct(array $array = [])
{
$this->array = $array;
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset)
{
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset)
{
unset($this->array[$offset]);
}
public function getIterator()
{
return new ArrayIterator($this->array);
}
public function toArray()
{
return $this->array;
}
}

421
src/Kernel/Support/Collection.php

@ -0,0 +1,421 @@ @@ -0,0 +1,421 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use ByteDance\Kernel\Contracts\Arrayable;
use Serializable;
/**
* Class Collection.
*/
class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Serializable, Arrayable
{
/**
* The collection data.
*
* @var array
*/
protected $items = [];
/**
* set data.
*
* @param mixed $items
*/
public function __construct(array $items = [])
{
foreach ($items as $key => $value) {
$this->set($key, $value);
}
}
/**
* Return all items.
*
* @return array
*/
public function all()
{
return $this->items;
}
/**
* Return specific items.
*
* @param array $keys
*
* @return \ByteDance\Kernel\Support\Collection
*/
public function only(array $keys)
{
$return = [];
foreach ($keys as $key) {
$value = $this->get($key);
if (!is_null($value)) {
$return[$key] = $value;
}
}
return new static($return);
}
/**
* Get all items except for those with the specified keys.
*
* @param mixed $keys
*
* @return static
*/
public function except($keys)
{
$keys = is_array($keys) ? $keys : func_get_args();
return new static(Arr::except($this->items, $keys));
}
/**
* Merge data.
*
* @param Collection|array $items
*
* @return \ByteDance\Kernel\Support\Collection
*/
public function merge($items)
{
$clone = new static($this->all());
foreach ($items as $key => $value) {
$clone->set($key, $value);
}
return $clone;
}
/**
* To determine Whether the specified element exists.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
return !is_null(Arr::get($this->items, $key));
}
/**
* Retrieve the first item.
*
* @return mixed
*/
public function first()
{
return reset($this->items);
}
/**
* Retrieve the last item.
*
* @return bool
*/
public function last()
{
$end = end($this->items);
reset($this->items);
return $end;
}
/**
* add the item value.
*
* @param string $key
* @param mixed $value
*/
public function add($key, $value)
{
Arr::set($this->items, $key, $value);
}
/**
* Set the item value.
*
* @param string $key
* @param mixed $value
*/
public function set($key, $value)
{
Arr::set($this->items, $key, $value);
}
/**
* Retrieve item from Collection.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public function get($key, $default = null)
{
return Arr::get($this->items, $key, $default);
}
/**
* Remove item form Collection.
*
* @param string $key
*/
public function forget($key)
{
Arr::forget($this->items, $key);
}
/**
* Build to array.
*
* @return array
*/
public function toArray()
{
return $this->all();
}
/**
* Build to json.
*
* @param int $option
*
* @return string
*/
public function toJson($option = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->all(), $option);
}
/**
* To string.
*
* @return string
*/
public function __toString()
{
return $this->toJson();
}
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON.
*
* @see http://php.net/manual/en/jsonserializable.jsonserialize.php
*
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource
*/
public function jsonSerialize()
{
return $this->items;
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* String representation of object.
*
* @see http://php.net/manual/en/serializable.serialize.php
*
* @return string the string representation of the object or null
*/
public function serialize()
{
return serialize($this->items);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Retrieve an external iterator.
*
* @see http://php.net/manual/en/iteratoraggregate.getiterator.php
*
* @return \ArrayIterator An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
*/
public function getIterator()
{
return new ArrayIterator($this->items);
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Count elements of an object.
*
* @see http://php.net/manual/en/countable.count.php
*
* @return int the custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer
*/
public function count()
{
return count($this->items);
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Constructs the object.
*
* @see http://php.net/manual/en/serializable.unserialize.php
*
* @param string $serialized <p>
* The string representation of the object.
* </p>
*
* @return mixed|void
*/
public function unserialize($serialized)
{
return $this->items = unserialize($serialized);
}
/**
* Get a data by key.
*
* @param string $key
*
* @return mixed
*/
public function __get($key)
{
return $this->get($key);
}
/**
* Assigns a value to the specified data.
*
* @param string $key
* @param mixed $value
*/
public function __set($key, $value)
{
$this->set($key, $value);
}
/**
* Whether or not an data exists by key.
*
* @param string $key
*
* @return bool
*/
public function __isset($key)
{
return $this->has($key);
}
/**
* Unset an data by key.
*
* @param string $key
*/
public function __unset($key)
{
$this->forget($key);
}
/**
* var_export.
*
* @return array
*/
public function __set_state()
{
return $this->all();
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Whether a offset exists.
*
* @see http://php.net/manual/en/arrayaccess.offsetexists.php
*
* @param mixed $offset <p>
* An offset to check for.
* </p>
*
* @return bool true on success or false on failure.
* The return value will be casted to boolean if non-boolean was returned
*/
public function offsetExists($offset)
{
return $this->has($offset);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Offset to unset.
*
* @see http://php.net/manual/en/arrayaccess.offsetunset.php
*
* @param mixed $offset <p>
* The offset to unset.
* </p>
*/
public function offsetUnset($offset)
{
if ($this->offsetExists($offset)) {
$this->forget($offset);
}
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Offset to retrieve.
*
* @see http://php.net/manual/en/arrayaccess.offsetget.php
*
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
*
* @return mixed Can return all value types
*/
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->get($offset) : null;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Offset to set.
*
* @see http://php.net/manual/en/arrayaccess.offsetset.php
*
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
*/
public function offsetSet($offset, $value)
{
$this->set($offset, $value);
}
}

133
src/Kernel/Support/File.php

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
use finfo;
/**
* Class File.
*/
class File
{
/**
* MIME mapping.
*
* @var array
*/
protected static $extensionMap = [
'audio/wav' => '.wav',
'audio/x-ms-wma' => '.wma',
'video/x-ms-wmv' => '.wmv',
'video/mp4' => '.mp4',
'audio/mpeg' => '.mp3',
'audio/amr' => '.amr',
'application/vnd.rn-realmedia' => '.rm',
'audio/mid' => '.mid',
'image/bmp' => '.bmp',
'image/gif' => '.gif',
'image/png' => '.png',
'image/tiff' => '.tiff',
'image/jpeg' => '.jpg',
'application/pdf' => '.pdf',
// 列举更多的文件 mime, 企业号是支持的,公众平台这边之后万一也更新了呢
'application/msword' => '.doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => '.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => '.dotx',
'application/vnd.ms-word.document.macroEnabled.12' => '.docm',
'application/vnd.ms-word.template.macroEnabled.12' => '.dotm',
'application/vnd.ms-excel' => '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => '.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => '.xltx',
'application/vnd.ms-excel.sheet.macroEnabled.12' => '.xlsm',
'application/vnd.ms-excel.template.macroEnabled.12' => '.xltm',
'application/vnd.ms-excel.addin.macroEnabled.12' => '.xlam',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => '.xlsb',
'application/vnd.ms-powerpoint' => '.ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => '.pptx',
'application/vnd.openxmlformats-officedocument.presentationml.template' => '.potx',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => '.ppsx',
'application/vnd.ms-powerpoint.addin.macroEnabled.12' => '.ppam',
];
/**
* File header signatures.
*
* @var array
*/
protected static $signatures = [
'ffd8ff' => '.jpg',
'424d' => '.bmp',
'47494638' => '.gif',
'2f55736572732f6f7665' => '.png',
'89504e47' => '.png',
'494433' => '.mp3',
'fffb' => '.mp3',
'fff3' => '.mp3',
'3026b2758e66cf11' => '.wma',
'52494646' => '.wav',
'57415645' => '.wav',
'41564920' => '.avi',
'000001ba' => '.mpg',
'000001b3' => '.mpg',
'2321414d52' => '.amr',
'25504446' => '.pdf',
];
/**
* Return steam extension.
*
* @param string $stream
*
* @return string|false
*/
public static function getStreamExt($stream)
{
try {
if (is_readable($stream)) {
$stream = file_get_contents($stream);
}
} catch (\Exception $e) {
}
$fileInfo = new finfo(FILEINFO_MIME);
$mime = strstr($fileInfo->buffer($stream), ';', true);
return isset(self::$extensionMap[$mime]) ? self::$extensionMap[$mime] : self::getExtBySignature($stream);
}
/**
* Get file extension by file header signature.
*
* @param string $stream
*
* @return string
*/
public static function getExtBySignature($stream)
{
$prefix = strval(bin2hex(mb_strcut($stream, 0, 10)));
foreach (self::$signatures as $signature => $extension) {
if (0 === strpos($prefix, strval($signature))) {
return $extension;
}
}
return '';
}
}

161
src/Kernel/Support/Helpers.php

@ -0,0 +1,161 @@ @@ -0,0 +1,161 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
/*
* helpers.
*
* @author overtrue <i@overtrue.me>
*/
/**
* Generate a signature.
*
* @param array $attributes
* @param string $key
* @param string $encryptMethod
*
* @return string
*/
function generate_sign(array $attributes, $key, $encryptMethod = 'md5')
{
$rList = array();
foreach ($attributes as $k => $v) {
if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
continue;
$value = trim(strval($v));
$len = strlen($value);
if ($len > 1 && substr($value, 0, 1) == "\"" && substr($value, $len, $len - 1) == "\"")
$value = substr($value, 1, $len - 1);
$value = trim($value);
if ($value == "" || $value == "null")
continue;
array_push($rList, $value);
}
array_push($rList, $key);
sort($rList, 2);
return call_user_func_array($encryptMethod, [implode('&', $rList)]);
}
/**
* @param $message
* @param $token
* @return string
*/
function generate_verify_sign($message, $token)
{
$data = [
$message['timestamp'],
(string)$message['nonce'],
(string)$message['msg'],
(string)$token
];
sort($data, SORT_STRING);
return sha1(join('', $data));
}
/**
* @param string $signType
* @param string $secretKey
*
* @return \Closure|string
*/
function get_encrypt_method(string $signType, string $secretKey = '')
{
if ('HMAC-SHA256' === $signType) {
return function ($str) use ($secretKey) {
return hash_hmac('sha256', $str, $secretKey);
};
}
return 'md5';
}
/**
* Get client ip.
*
* @return string
*/
function get_client_ip()
{
if (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
} else {
// for php-cli(phpunit etc.)
$ip = defined('PHPUNIT_RUNNING') ? '127.0.0.1' : gethostbyname(gethostname());
}
return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1';
}
/**
* Get current server ip.
*
* @return string
*/
function get_server_ip()
{
if (!empty($_SERVER['SERVER_ADDR'])) {
$ip = $_SERVER['SERVER_ADDR'];
} elseif (!empty($_SERVER['SERVER_NAME'])) {
$ip = gethostbyname($_SERVER['SERVER_NAME']);
} else {
// for php-cli(phpunit etc.)
$ip = defined('PHPUNIT_RUNNING') ? '127.0.0.1' : gethostbyname(gethostname());
}
return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1';
}
/**
* Return current url.
*
* @return string
*/
function current_url()
{
$protocol = 'http://';
if ((!empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS']) || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'http') === 'https') {
$protocol = 'https://';
}
return $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
/**
* Return random string.
*
* @param string $length
*
* @return string
* @throws \ByteDance\Kernel\Exceptions\RuntimeException
*/
function str_random($length)
{
return Str::random($length);
}
/**
* @param string $content
* @param string $publicKey
*
* @return string
*/
function rsa_public_encrypt($content, $publicKey)
{
$encrypted = '';
openssl_public_encrypt($content, $encrypted, openssl_pkey_get_public($publicKey), OPENSSL_PKCS1_OAEP_PADDING);
return base64_encode($encrypted);
}

194
src/Kernel/Support/Str.php

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Support;
use ByteDance\Kernel\Exceptions\RuntimeException;
/**
* Class Str.
*/
class Str
{
/**
* The cache of snake-cased words.
*
* @var array
*/
protected static $snakeCache = [];
/**
* The cache of camel-cased words.
*
* @var array
*/
protected static $camelCache = [];
/**
* The cache of studly-cased words.
*
* @var array
*/
protected static $studlyCache = [];
/**
* Convert a value to camel case.
*
* @param string $value
*
* @return string
*/
public static function camel($value)
{
if (isset(static::$camelCache[$value])) {
return static::$camelCache[$value];
}
return static::$camelCache[$value] = lcfirst(static::studly($value));
}
/**
* Generate a more truly "random" alpha-numeric string.
*
* @param int $length
*
* @return string
*
* @throws \ByteDance\Kernel\Exceptions\RuntimeException
*/
public static function random($length = 16)
{
$string = '';
while (($len = strlen($string)) < $length) {
$size = $length - $len;
$bytes = static::randomBytes($size);
$string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);
}
return $string;
}
/**
* Generate a more truly "random" bytes.
*
* @param int $length
*
* @throws RuntimeException
* @throws \Exception
*
* @return string
*
*
* @codeCoverageIgnore
*/
public static function randomBytes($length = 16)
{
if (function_exists('random_bytes')) {
$bytes = random_bytes($length);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$bytes = openssl_random_pseudo_bytes($length, $strong);
if (false === $bytes || false === $strong) {
throw new RuntimeException('Unable to generate random string.');
}
} else {
throw new RuntimeException('OpenSSL extension is required for PHP 5 users.');
}
return $bytes;
}
/**
* Generate a "random" alpha-numeric string.
*
* Should not be considered sufficient for cryptography, etc.
*
* @param int $length
*
* @return string
*/
public static function quickRandom($length = 16)
{
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return substr(str_shuffle(str_repeat($pool, $length)), 0, $length);
}
/**
* Convert the given string to upper-case.
*
* @param string $value
*
* @return string
*/
public static function upper($value)
{
return mb_strtoupper($value);
}
/**
* Convert the given string to title case.
*
* @param string $value
*
* @return string
*/
public static function title($value)
{
return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
}
/**
* Convert a string to snake case.
*
* @param string $value
* @param string $delimiter
*
* @return string
*/
public static function snake($value, $delimiter = '_')
{
$key = $value.$delimiter;
if (isset(static::$snakeCache[$key])) {
return static::$snakeCache[$key];
}
if (!ctype_lower($value)) {
$value = strtolower(preg_replace('/(.)(?=[A-Z])/', '$1'.$delimiter, $value));
}
return static::$snakeCache[$key] = trim($value, '_');
}
/**
* Convert a value to studly caps case.
*
* @param string $value
*
* @return string
*/
public static function studly($value)
{
$key = $value;
if (isset(static::$studlyCache[$key])) {
return static::$studlyCache[$key];
}
$value = ucwords(str_replace(['-', '_'], ' ', $value));
return static::$studlyCache[$key] = str_replace(' ', '', $value);
}
}

247
src/Kernel/Traits/HasAttributes.php

@ -0,0 +1,247 @@ @@ -0,0 +1,247 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Traits;
use ByteDance\Kernel\Exceptions\InvalidArgumentException;
use ByteDance\Kernel\Support\Arr;
use ByteDance\Kernel\Support\Str;
/**
* Trait Attributes.
*/
trait HasAttributes
{
/**
* @var array
*/
protected $attributes = [];
/**
* @var bool
*/
protected $snakeable = true;
/**
* Set Attributes.
*
* @param array $attributes
*
* @return $this
*/
public function setAttributes(array $attributes = [])
{
$this->attributes = $attributes;
return $this;
}
/**
* Set attribute.
*
* @param string $attribute
* @param string $value
*
* @return $this
*/
public function setAttribute($attribute, $value)
{
Arr::set($this->attributes, $attribute, $value);
return $this;
}
/**
* Get attribute.
*
* @param string $attribute
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($attribute, $default = null)
{
return Arr::get($this->attributes, $attribute, $default);
}
/**
* @param string $attribute
*
* @return bool
*/
public function isRequired($attribute)
{
return in_array($attribute, $this->getRequired(), true);
}
/**
* @return array|mixed
*/
public function getRequired()
{
return property_exists($this, 'required') ? $this->required : [];
}
/**
* Set attribute.
*
* @param string $attribute
* @param mixed $value
*
* @return $this
*/
public function with($attribute, $value)
{
$this->snakeable && $attribute = Str::snake($attribute);
$this->setAttribute($attribute, $value);
return $this;
}
/**
* Override parent set() method.
*
* @param string $attribute
* @param mixed $value
*
* @return $this
*/
public function set($attribute, $value)
{
$this->setAttribute($attribute, $value);
return $this;
}
/**
* Override parent get() method.
*
* @param string $attribute
* @param mixed $default
*
* @return mixed
*/
public function get($attribute, $default = null)
{
return $this->getAttribute($attribute, $default);
}
/**
* @param string $key
*
* @return bool
*/
public function has(string $key)
{
return Arr::has($this->attributes, $key);
}
/**
* @param array $attributes
*
* @return $this
*/
public function merge(array $attributes)
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
/**
* @param array|string $keys
*
* @return array
*/
public function only($keys)
{
return Arr::only($this->attributes, $keys);
}
/**
* Return all items.
*
* @return array
* @throws InvalidArgumentException
*/
public function all()
{
$this->checkRequiredAttributes();
return $this->attributes;
}
/**
* Magic call.
*
* @param string $method
* @param array $args
*
* @return $this
*/
public function __call($method, $args)
{
if (0 === stripos($method, 'with')) {
return $this->with(substr($method, 4), array_shift($args));
}
throw new \BadMethodCallException(sprintf('Method "%s" does not exists.', $method));
}
/**
* Magic get.
*
* @param string $property
*
* @return mixed
*/
public function __get($property)
{
return $this->get($property);
}
/**
* Magic set.
*
* @param string $property
* @param mixed $value
*
* @return $this
*/
public function __set($property, $value)
{
return $this->with($property, $value);
}
/**
* Whether or not an data exists by key.
*
* @param string $key
*
* @return bool
*/
public function __isset($key)
{
return isset($this->attributes[$key]);
}
/**
* Check required attributes.
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*/
protected function checkRequiredAttributes()
{
foreach ($this->getRequired() as $attribute) {
if (is_null($this->get($attribute))) {
throw new InvalidArgumentException(sprintf('"%s" cannot be empty.', $attribute));
}
}
}
}

217
src/Kernel/Traits/HasHttpRequests.php

@ -0,0 +1,217 @@ @@ -0,0 +1,217 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Traits;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\ResponseInterface;
/**
* Trait HasHttpRequests.
*
* @author overtrue <i@overtrue.me>
*/
trait HasHttpRequests
{
use ResponseCastable;
/**
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* @var array
*/
protected $middlewares = [];
/**
* @var \GuzzleHttp\HandlerStack
*/
protected $handlerStack;
/**
* @var array
*/
protected static $defaults = [
'curl' => [
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
],
];
/**
* Set guzzle default settings.
*
* @param array $defaults
*/
public static function setDefaultOptions($defaults = [])
{
self::$defaults = $defaults;
}
/**
* Return current guzzle default settings.
*
* @return array
*/
public static function getDefaultOptions(): array
{
return self::$defaults;
}
/**
* Set GuzzleHttp\Client.
*
* @param \GuzzleHttp\ClientInterface $httpClient
*
* @return $this
*/
public function setHttpClient(ClientInterface $httpClient)
{
$this->httpClient = $httpClient;
return $this;
}
/**
* Return GuzzleHttp\ClientInterface instance.
*
* @return ClientInterface
*/
public function getHttpClient(): ClientInterface
{
if (!($this->httpClient instanceof ClientInterface)) {
if (property_exists($this, 'app') && $this->app['http_client']) {
$this->httpClient = $this->app['http_client'];
} else {
$this->httpClient = new Client(['handler' => HandlerStack::create($this->getGuzzleHandler())]);
}
}
return $this->httpClient;
}
/**
* Add a middleware.
*
* @param callable $middleware
* @param string|null $name
*
* @return $this
*/
public function pushMiddleware(callable $middleware, string $name = null)
{
if (!is_null($name)) {
$this->middlewares[$name] = $middleware;
} else {
array_push($this->middlewares, $middleware);
}
return $this;
}
/**
* Return all middlewares.
*
* @return array
*/
public function getMiddlewares(): array
{
return $this->middlewares;
}
/**
* Make a request.
*
* @param string $url
* @param string $method
* @param array $options
*
* @return \Psr\Http\Message\ResponseInterface|\ByteDance\Kernel\Support\Collection|array|object|string
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function request($url, $method = 'GET', $options = []): ResponseInterface
{
$method = strtoupper($method);
$options = array_merge(self::$defaults, $options, ['handler' => $this->getHandlerStack()]);
$options = $this->fixJsonIssue($options);
if (property_exists($this, 'baseUri') && !is_null($this->baseUri)) {
$options['base_uri'] = $this->baseUri;
}
$response = $this->getHttpClient()->request($method, $url, $options);
$response->getBody()->rewind();
return $response;
}
/**
* @param \GuzzleHttp\HandlerStack $handlerStack
*
* @return $this
*/
public function setHandlerStack(HandlerStack $handlerStack)
{
$this->handlerStack = $handlerStack;
return $this;
}
/**
* Build a handler stack.
*
* @return \GuzzleHttp\HandlerStack
*/
public function getHandlerStack(): HandlerStack
{
if ($this->handlerStack) {
return $this->handlerStack;
}
$this->handlerStack = HandlerStack::create($this->getGuzzleHandler());
foreach ($this->middlewares as $name => $middleware) {
$this->handlerStack->push($middleware, $name);
}
return $this->handlerStack;
}
/**
* @param array $options
*
* @return array
*/
protected function fixJsonIssue(array $options): array
{
if (isset($options['json']) && is_array($options['json'])) {
$options['headers'] = array_merge($options['headers'] ?? [], ['Content-Type' => 'application/json']);
if (empty($options['json'])) {
$options['body'] = \GuzzleHttp\json_encode($options['json'], JSON_FORCE_OBJECT);
} else {
$options['body'] = \GuzzleHttp\json_encode($options['json'], JSON_UNESCAPED_UNICODE);
}
unset($options['json']);
}
return $options;
}
/**
* Get guzzle handler.
*
* @return callable
*/
protected function getGuzzleHandler()
{
if (property_exists($this, 'app') && isset($this->app['guzzle_handler']) && is_string($this->app['guzzle_handler'])) {
$handler = $this->app['guzzle_handler'];
return new $handler();
}
return \GuzzleHttp\choose_handler();
}
}

83
src/Kernel/Traits/InteractsWithCache.php

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Traits;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\FilesystemCache;
use ByteDance\Kernel\ServiceContainer;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
/**
* Trait InteractsWithCache.
*
* @author overtrue <i@overtrue.me>
*/
trait InteractsWithCache
{
/**
* @var \Psr\SimpleCache\CacheInterface
*/
protected $cache;
/**
* Get cache instance.
*
* @return \Psr\SimpleCache\CacheInterface
*/
public function getCache()
{
if ($this->cache) {
return $this->cache;
}
if (property_exists($this, 'app') && $this->app instanceof ServiceContainer
&& isset($this->app['cache']) && $this->app['cache'] instanceof CacheInterface) {
return $this->cache = $this->app['cache'];
}
return $this->cache = $this->createDefaultCache();
}
/**
* Set cache instance.
*
* @param \Psr\SimpleCache\CacheInterface $cache
*
* @return $this
*/
public function setCache(CacheInterface $cache)
{
$this->cache = $cache;
return $this;
}
/**
* @return Psr16Cache|\Symfony\Component\Cache\Simple\FilesystemCache
*/
protected function createDefaultCache()
{
if ($this->isSymfony43()) {
return new Psr16Cache(new FilesystemAdapter('bytedance', 1500));
}
return new \Symfony\Component\Cache\Simple\FilesystemCache();
}
/**
* @return bool
*/
protected function isSymfony43(): bool
{
return \class_exists('Symfony\Component\Cache\Psr16Cache');
}
}

241
src/Kernel/Traits/Observable.php

@ -0,0 +1,241 @@ @@ -0,0 +1,241 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Traits;
use ByteDance\Kernel\Clauses\Clause;
use ByteDance\Kernel\Contracts\EventHandlerInterface;
use ByteDance\Kernel\Decorators\FinallyResult;
use ByteDance\Kernel\Decorators\TerminateResult;
use ByteDance\Kernel\Exceptions\InvalidArgumentException;
use ByteDance\Kernel\ServiceContainer;
/**
* Trait Observable.
*
* @author overtrue <i@overtrue.me>
*/
trait Observable
{
/**
* @var array
*/
protected $handlers = [];
/**
* @var array
*/
protected $clauses = [];
/**
* @param \Closure|EventHandlerInterface|string $handler
* @param \Closure|EventHandlerInterface|string $condition
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*
* @return \ByteDance\Kernel\Clauses\Clause
*/
public function push($handler, $condition = '*')
{
list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition);
if (!isset($this->handlers[$condition])) {
$this->handlers[$condition] = [];
}
array_push($this->handlers[$condition], $handler);
return $this->newClause($handler);
}
/**
* @param \Closure|EventHandlerInterface|string $handler
* @param \Closure|EventHandlerInterface|string $condition
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*
* @return \ByteDance\Kernel\Clauses\Clause
*/
public function unshift($handler, $condition = '*')
{
list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition);
if (!isset($this->handlers[$condition])) {
$this->handlers[$condition] = [];
}
array_unshift($this->handlers[$condition], $handler);
return $this->newClause($handler);
}
/**
* @param string $condition
* @param \Closure|EventHandlerInterface|string $handler
*
* @return \ByteDance\Kernel\Clauses\Clause
* @throws \ReflectionException
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*/
public function observe($condition, $handler)
{
return $this->push($handler, $condition);
}
/**
* @param string $condition
* @param \Closure|EventHandlerInterface|string $handler
*
* @return \ByteDance\Kernel\Clauses\Clause
* @throws \ReflectionException
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*/
public function on($condition, $handler)
{
return $this->push($handler, $condition);
}
/**
* @param string|int $event
* @param mixed ...$payload
*
* @return mixed|null
*/
public function dispatch($event, $payload)
{
return $this->notify($event, $payload);
}
/**
* @param string|int $event
* @param mixed ...$payload
*
* @return mixed|null
*/
public function notify($event, $payload)
{
$result = null;
foreach ($this->handlers as $condition => $handlers) {
if ('*' === $condition || ($condition & $event) === $event) {
foreach ($handlers as $handler) {
if ($clause = $this->clauses[spl_object_hash((object) $handler)] ?? null) {
if ($clause->intercepted($payload)) {
continue;
}
}
$response = $this->callHandler($handler, $payload);
switch (true) {
case $response instanceof TerminateResult:
return $response->content;
case true === $response:
continue 2;
case false === $response:
break 2;
case !empty($response) && !($result instanceof FinallyResult):
$result = $response;
}
}
}
}
return $result instanceof FinallyResult ? $result->content : $result;
}
/**
* @return array
*/
public function getHandlers()
{
return $this->handlers;
}
/**
* @param mixed $handler
*
* @return \ByteDance\Kernel\Clauses\Clause
*/
protected function newClause($handler): Clause
{
return $this->clauses[spl_object_hash((object) $handler)] = new Clause();
}
/**
* @param callable $handler
* @param mixed $payload
*
* @return mixed
*/
protected function callHandler(callable $handler, $payload)
{
try {
return $handler($payload);
} catch (\Exception $e) {
if (property_exists($this, 'app') && $this->app instanceof ServiceContainer) {
$this->app['logger']->error($e->getCode().': '.$e->getMessage(), [
'code' => $e->getCode(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}
}
}
/**
* @param $handler
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*
* @return \Closure
*/
protected function makeClosure($handler)
{
if (is_callable($handler)) {
return $handler;
}
if (is_string($handler)) {
if (!class_exists($handler)) {
throw new InvalidArgumentException(sprintf('Class "%s" not exists.', $handler));
}
if (!in_array(EventHandlerInterface::class, (new \ReflectionClass($handler))->getInterfaceNames(), true)) {
throw new InvalidArgumentException(sprintf('Class "%s" not an instance of "%s".', $handler, EventHandlerInterface::class));
}
return function ($payload) use ($handler) {
return (new $handler($this->app ?? null))->handle($payload);
};
}
if ($handler instanceof EventHandlerInterface) {
return function () use ($handler) {
return $handler->handle(...func_get_args());
};
}
throw new InvalidArgumentException('No valid handler is found in arguments.');
}
/**
* @param $handler
* @param $condition
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*
* @return array
*/
protected function resolveHandlerAndCondition($handler, $condition): array
{
if (is_int($handler) || (is_string($handler) && !class_exists($handler))) {
list($handler, $condition) = [$condition, $handler];
}
return [$this->makeClosure($handler), $condition];
}
}

89
src/Kernel/Traits/ResponseCastable.php

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Kernel\Traits;
use ByteDance\Kernel\Contracts\Arrayable;
use ByteDance\Kernel\Exceptions\InvalidArgumentException;
use ByteDance\Kernel\Http\Response;
use ByteDance\Kernel\Support\Collection;
use Psr\Http\Message\ResponseInterface;
/**
* Trait ResponseCastable.
*
* @author overtrue <i@overtrue.me>
*/
trait ResponseCastable
{
/**
* @param \Psr\Http\Message\ResponseInterface $response
* @param string|null $type
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*/
protected function castResponseToType(ResponseInterface $response, $type = null)
{
$response = Response::buildFromPsrResponse($response);
$response->getBody()->rewind();
switch ($type ?? 'array') {
case 'collection':
return $response->toCollection();
case 'array':
return $response->toArray();
case 'object':
return $response->toObject();
case 'raw':
return $response;
default:
if (!is_subclass_of($type, Arrayable::class)) {
throw new InvalidArgumentException(sprintf(
'Config key "response_type" classname must be an instanceof %s',
Arrayable::class
));
}
return new $type($response);
}
}
/**
* @param mixed $response
* @param string|null $type
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
*
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*/
protected function detectAndCastResponseToType($response, $type = null)
{
switch (true) {
case $response instanceof ResponseInterface:
$response = Response::buildFromPsrResponse($response);
break;
case $response instanceof Arrayable:
$response = new Response(200, [], json_encode($response->toArray()));
break;
case ($response instanceof Collection) || is_array($response) || is_object($response):
$response = new Response(200, [], json_encode($response));
break;
case is_scalar($response):
$response = new Response(200, [], $response);
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported response type "%s"', gettype($response)));
}
return $this->castResponseToType($response, $type);
}
}

38
src/MiniProgram/Application.php

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram;
use ByteDance\Kernel\ServiceContainer;
/**
* Class Application.
*
* @property \ByteDance\MiniProgram\Auth\AccessToken $access_token
* @property \ByteDance\MiniProgram\Auth\Client $auth
* @property \ByteDance\MiniProgram\KVData\Client $kv
* @property \ByteDance\MiniProgram\Encryptor $encryptor
* @property \ByteDance\MiniProgram\QRCode\Client $qrcode
* @property \ByteDance\MiniProgram\Message\Client $message
* @property \ByteDance\MiniProgram\Payment\Client $payment
*/
class Application extends ServiceContainer
{
/**
* @var array
*/
protected $providers = [
Auth\ServiceProvider::class,
KVData\ServiceProvider::class,
QRCode\ServiceProvider::class,
Message\ServiceProvider::class,
Payment\ServiceProvider::class,
];
}

43
src/MiniProgram/Auth/AccessToken.php

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Auth;
use ByteDance\Kernel\AccessToken as BaseAccessToken;
/**
* Class AuthorizerAccessToken.
*
* @author overtrue <i@overtrue.me>
*/
class AccessToken extends BaseAccessToken
{
/**
* @var string
*/
protected $endpointToGetToken = 'https://developer.toutiao.com/api/apps/token';
/**
* @return array
*/
protected function getCredentials(): array
{
$client_secret = $this->app['config']['app_secret'];
date_default_timezone_set('Asia/Shanghai');
$client_id = $this->app['config']['app_id'];
$grand_type = 'client_credential';
return [
'grant_type' => $grand_type,
'appid' => $client_id,
'secret' => $client_secret,
];
}
}

46
src/MiniProgram/Auth/Client.php

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Auth;
use ByteDance\Kernel\BaseClient;
/**
* Class Auth.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class Client extends BaseClient
{
/**
* Get session info by code.
* @param string $code
* @param bool $anonymous
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function session(string $code, bool $anonymous = false)
{
$params = [
'appid' => $this->app['config']['app_id'],
'secret' => $this->app['config']['app_secret'],
];
if ($anonymous) {
$params['anonymous_code'] = $code;
} else {
$params['code'] = $code;
}
return $this->httpGet('api/apps/jscode2session', $params);
}
}

40
src/MiniProgram/Auth/ServiceProvider.php

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Auth;
use ByteDance\MiniProgram\Encryptor;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['access_token']) && $app['access_token'] = function ($app) {
return new AccessToken($app);
};
!isset($app['auth']) && $app['auth'] = function ($app) {
return new Client($app);
};
!isset($app['encryptor']) && $app['encryptor'] = function ($app) {
return new Encryptor();
};
}
}

48
src/MiniProgram/Encryptor.php

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram;
use ByteDance\Kernel\Encryptor as BaseEncryptor;
use ByteDance\Kernel\Exceptions\DecryptException;
use ByteDance\Kernel\Support\AES;
/**
* Class Encryptor.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class Encryptor extends BaseEncryptor
{
/**
* Decrypt data.
* @param string $sessionKey
* @param string $iv
* @param string $encrypted
* @return array
* @throws DecryptException
*/
public function decryptData(string $sessionKey, string $iv, string $encrypted): array
{
$decrypted = AES::decrypt(
base64_decode($encrypted, false), base64_decode($sessionKey, false), base64_decode($iv, false)
);
$decrypted = json_decode($this->pkcs7Unpad($decrypted), true);
if (!$decrypted) {
throw new DecryptException('The given payload is invalid.');
}
return $decrypted;
}
}

68
src/MiniProgram/KVData/Client.php

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\KVData;
use ByteDance\Kernel\BaseClient;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
* Set User Storage
* 当 key 是开发者所配置的排行榜 key 时,value 的内容应该满足KVData所指出的形式, 即形如 "{\"ttgame\":{\"score\":1}}".
*
* openid 登录用户唯一标识
* signature 用户登录态签名,参考用户登录态签名算法
* sig_method 用户登录态签名的编码方法,参考用户登录态签名算法
* kv_list (body 中) 要设置的用户数据
*
* @param string $openid
* @param string $signature
* @param string $sig_method
* @param string $kv_list
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function set(string $openid, string $signature, string $sig_method, string $kv_list)
{
return $this->httpPost('api/apps/set_user_storage', ['openid'=>$openid, 'signature' => $signature, 'sig_method'=>$sig_method, 'kv_list'=>$kv_list]);
}
/**
* Set User Storage
* 当 key 是开发者所配置的排行榜 key 时,value 的内容应该满足KVData所指出的形式, 即形如 "{\"ttgame\":{\"score\":1}}".
*
* openid 登录用户唯一标识
* signature 用户登录态签名,参考用户登录态签名算法
* sig_method 用户登录态签名的编码方法,参考用户登录态签名算法
* key (body 中) 要删除的用户数据的 key list
*
* @param string $openid
* @param string $signature
* @param string $sig_method
* @param string $key
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function remove(string $openid, string $signature, string $sig_method, string $key)
{
return $this->httpPost('api/apps/remove_user_storage', ['openid'=>$openid, 'signature' => $signature, 'sig_method'=>$sig_method, 'key'=>$key]);
}
}

32
src/MiniProgram/KVData/ServiceProvider.php

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\KVData;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['kv']) && $app['kv'] = function ($app) {
return new Client($app);
};
}
}

55
src/MiniProgram/Message/Client.php

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Message;
use ByteDance\Kernel\BaseClient;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
*
* 发送模版消息
* 目前只有今日头条支持,抖音和 lite 接入中
*
* touser String 是 要发送给用户的 open id, open id 的获取请参考登录
* template_id String 是 在开发者平台配置消息模版后获得的模版 id
* page String 否 点击消息卡片之后打开的小程序页面地址,空则无跳转
* form_id String 是 可以通过<form />组件获得 form_id, 获取方法
* data dict<String, SubData> 是 模板中填充着的数据,key 必须是 keyword 为前缀
*SubData
*SubData 也是 dict,结构如下:
*名称 类型 是否必填
*value String 是
* eg: {"access_token": "YOUR_ACCESS_TOKEN", "app_id": "YOUR_APP_ID", "data": {"keyword1": {"value": "v1"}, "keyword2": {"value": "v2"}}, "page": "pages/index", "form_id": "YOUR_FORM_ID", "touser": "USER_OPEN_ID", "template_id": "YOUR_TPL_ID"}
*
* @param string $touser
* @param string $template_id
* @param string $page
* @param string $form_id
* @param mixed $data
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function create(string $touser, string $template_id, string $page, string $form_id, mixed $data)
{
return $this->httpPost('api/apps/game/template/send', ['touser'=>$touser, 'template_id' => $template_id, 'page'=>$page, 'form_id'=>$form_id, 'data'=>$data]);
}
}

30
src/MiniProgram/Message/ServiceProvider.php

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Message;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['message']) && $app['message'] = function ($app) {
return new Client($app);
};
}
}

91
src/MiniProgram/Payment/Client.php

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Payment;
use ByteDance\Kernel\BaseClient;
use ByteDance\Kernel\Support;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
* @param string $uid
* @param string $outTradeNo
* @param int $totalAmount
* @param string $subject
* @param array $optional
* @return array
*/
public function pay(string $uid, string $outTradeNo, int $totalAmount, string $subject, array $optional = [])
{
$appId = $this->app['config']['pay_app_id'];
$mchId = $this->app['config']['mch_id'];
$secretKey = $this->app['config']['key'];
$params = [
'app_id' => $appId,
'merchant_id' => $mchId,
'timestamp' => $time = (string)time(),
'sign_type' => 'MD5',
'out_order_no' => $outTradeNo,
'total_amount' => $totalAmount,
'product_code' => 'pay',
'payment_type' => 'direct',
'trade_type' => 'H5',
'version' => '2.0',
'currency' => 'CNY',
'subject' => $subject,
'body' => $optional['body'] ?? $subject,
'uid' => $uid,
'trade_time' => $time,
'valid_time' => $optional['valid_time'] ?? '300',
'notify_url' => $optional['notify_url'] ?? 'https://www.baidu.com'
];
if (!empty($optional['wx_url'])) {
$params['wx_type'] = 'MWEB';
$params['wx_url'] = $optional['wx_url'];
}
if (!empty($optional['alipay_url'])) {
$params['alipay_url'] = $optional['alipay_url'];
}
$params['sign'] = $this->sign($params, $secretKey);
$params['risk_info'] = $optional['risk_info'] ?? json_encode(['ip' => $ip = Support\get_client_ip()]);
return $params;
}
/**
* @param array $data
* @param string $secretKey
* @return string
*/
public function sign(array $data, string $secretKey)
{
$signData = '';
ksort($data);
foreach ($data as $k => $v) {
if (is_array($v)) {
$value = json_encode($v);
} else {
$value = $v;
}
if ($value) {
$signData .= '&' . $k . '=' . $value;
}
}
$signData = ltrim($signData, '&');
return md5($signData . $secretKey);
}
}

32
src/MiniProgram/Payment/ServiceProvider.php

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\Payment;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['payment']) && $app['payment'] = function ($app) {
return new Client($app);
};
}
}

41
src/MiniProgram/QRCode/Client.php

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\QRCode;
use ByteDance\Kernel\BaseClient;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
* @param array $params
* @return mixed|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function create(array $params = [])
{
$response= $this->httpPostJson('api/apps/qrcode',$params);
$responses = $response->getBody()->getContents();
$res = @json_decode($responses,1);
if ($res){
return $res;
}else{
return $responses;
}
}
}

32
src/MiniProgram/QRCode/ServiceProvider.php

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\MiniProgram\QRCode;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['qrcode']) && $app['qrcode'] = function ($app) {
return new Client($app);
};
}
}

69
src/Payment/Application.php

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment;
use ByteDance\Kernel\ServiceContainer;
use Closure;
/**
* Class Application.
*
* @property \ByteDance\Payment\Order\Client $order
* @property \ByteDance\Payment\Refund\Client $refund
*/
class Application extends ServiceContainer
{
/**
* @var array
*/
protected $providers = [
Order\ServiceProvider::class,
Refund\ServiceProvider::class
];
/**
* @var array
*/
protected $defaultConfig = [
'http' => [
'base_uri' => 'https://developer.toutiao.com/api/apps/',
],
];
/**
* Set sub-merchant.
*
* @param string $mchId
* @param string|null $appId
*
* @return $this
*/
public function setSubMerchant(string $mchId)
{
$this['config']->set('thirdparty_id', $mchId);
return $this;
}
public function getKey()
{
return $this['config']->key;
}
/**
* 支付回调
* @param Closure $closure
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handlePaidNotify(Closure $closure)
{
return (new Notify\Paid($this))->handle($closure);
}
}

189
src/Payment/Kernel/BaseClient.php

@ -0,0 +1,189 @@ @@ -0,0 +1,189 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Kernel;
use ByteDance\Kernel\Support;
use ByteDance\Kernel\Traits\HasHttpRequests;
use ByteDance\Payment\Application;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;
/**
* Class BaseClient.
*
* @author overtrue <i@overtrue.me>
*/
class BaseClient
{
use HasHttpRequests {
request as performRequest;
}
/**
* @var \ByteDance\Payment\Application
*/
protected $app;
/**
* Constructor.
*
* @param \ByteDance\Payment\Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
$this->setHttpClient($this->app['http_client']);
}
/**
* Extra request params.
*
* @return array
*/
protected function prepends()
{
return [];
}
/**
* Make a API request.
*
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
* @param bool $returnResponse
*
* @return \Psr\Http\Message\ResponseInterface|\ByteDance\Kernel\Support\Collection|array|object|string
*
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function request(string $endpoint, array $params = [], $method = 'post', array $options = [], $returnResponse = false)
{
$base = [
'app_id' => $this->app['config']['app_id'],
'thirdparty_id' => $this->app['config']['thirdparty_id']
];
$params = array_filter(array_merge($base, $this->prepends(), $params), 'strlen');
$secretKey = $this->app->getKey();
// $encryptMethod = Support\get_encrypt_method(Support\Arr::get($params, 'sign_type', 'MD5'), $secretKey);
$params['sign'] = Support\generate_sign($params, $secretKey);
$options = array_merge([
'json' => $params,
], $options);
$this->pushMiddleware($this->logMiddleware(), 'log');
$response = $this->performRequest($endpoint, $method, $options);
return $returnResponse ? $response : $this->castResponseToType($response, $this->app->config->get('response_type'));
}
/**
* Log the request.
*
* @return \Closure
*/
protected function logMiddleware()
{
$formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG);
return Middleware::log($this->app['logger'], $formatter);
}
/**
* Make a request and return raw response.
*
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
*
* @return ResponseInterface
*
* @throws \HttpBase\Exceptions\InvalidConfigException
* @throws \HttpBase\Exceptions\InvalidArgumentException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function requestRaw(string $endpoint, array $params = [], $method = 'post', array $options = [])
{
/** @var ResponseInterface $response */
$response = $this->request($endpoint, $params, $method, $options, true);
return $response;
}
/**
* Make a request and return an array.
*
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
*
* @return array
*
* @throws \HttpBase\Exceptions\InvalidArgumentException
* @throws \HttpBase\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function requestArray(string $endpoint, array $params = [], $method = 'post', array $options = []): array
{
$response = $this->requestRaw($endpoint, $params, $method, $options);
return $this->castResponseToType($response, 'array');
}
/**
* Request with SSL.
*
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
*
* @return \Psr\Http\Message\ResponseInterface|\Wechat\Kernel\Support\Collection|array|object|string
*
* @throws \HttpBase\Exceptions\InvalidConfigException
* @throws \HttpBase\Exceptions\InvalidArgumentException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function safeRequest($endpoint, array $params, $method = 'post', array $options = [])
{
$options = array_merge([
'cert' => $this->app['config']->get('cert_path'),
'ssl_key' => $this->app['config']->get('key_path'),
], $options);
return $this->request($endpoint, $params, $method, $options);
}
/**
* Wrapping an API endpoint.
*
* @param string $endpoint
*
* @return string
*/
protected function wrap(string $endpoint): string
{
return $this->app->inSandbox() ? "sandboxnew/{$endpoint}" : $endpoint;
}
}

18
src/Payment/Kernel/Exceptions/InvalidSignException.php

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Kernel\Exceptions;
use ByteDance\Kernel\Exceptions\Exception;
class InvalidSignException extends Exception
{
}

18
src/Payment/Kernel/Exceptions/SandboxException.php

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Kernel\Exceptions;
use ByteDance\Kernel\Exceptions\Exception;
class SandboxException extends Exception
{
}

197
src/Payment/Notify/Handler.php

@ -0,0 +1,197 @@ @@ -0,0 +1,197 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Notify;
use Closure;
use ByteDance\Kernel\Exceptions\Exception;
use ByteDance\Kernel\Support;
use ByteDance\Payment\Kernel\Exceptions\InvalidSignException;
use Symfony\Component\HttpFoundation\Response;
abstract class Handler
{
const SUCCESS = 'success';
const FAIL = 'fail';
/**
* @var \ByteDance\Payment\Application
*/
protected $app;
/**
* @var array
*/
protected $message;
/**
* @var string|null
*/
protected $fail;
/**
* @var array
*/
protected $attributes = [];
/**
* Check sign.
* If failed, throws an exception.
*
* @var bool
*/
protected $check = true;
/**
* Respond with sign.
*
* @var bool
*/
protected $sign = false;
/**
* @param \ByteDance\Payment\Application $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Handle incoming notify.
*
* @param \Closure $closure
*
* @return \Symfony\Component\HttpFoundation\Response
*/
abstract public function handle(Closure $closure);
/**
* @param string $message
*/
public function fail(string $message)
{
$this->fail = $message;
}
/**
* @param array $attributes
* @param bool $sign
*
* @return $this
*/
public function respondWith(array $attributes, bool $sign = false)
{
$this->attributes = $attributes;
$this->sign = $sign;
return $this;
}
/**
* Build xml and return the response to WeChat.
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*/
public function toResponse(): Response
{
$base = [
'err_no' => is_null($this->fail) ? 0 : 400,
'err_tips' => is_null($this->fail) ? static::SUCCESS : static::FAIL,
];
return new Response(json_encode($base));
}
/**
* Return the notify message from request.
*
* @return array
*
* @throws \ByteDance\Kernel\Exceptions\Exception
*/
public function getMessage(): array
{
if (!empty($this->message)) {
return $this->message;
}
try {
$message = json_decode(strval($this->app['request']->getContent()), true);
} catch (\Throwable $e) {
throw new Exception('Invalid request json: ' . $e->getMessage(), 400);
}
if (!is_array($message) || empty($message)) {
throw new Exception('Invalid request json.', 400);
}
if ($this->check) {
$this->validate($message);
}
return $this->message = json_decode($message['msg'], true);
}
/**
* Decrypt message.
*
* @param string $key
*
* @return string|null
*
* @throws \ByteDance\Kernel\Exceptions\Exception
*/
public function decryptMessage(string $key)
{
$message = $this->getMessage();
if (empty($message[$key])) {
return null;
}
return Support\AES::decrypt(
base64_decode($message[$key], true),
md5($this->app['config']->key),
'',
OPENSSL_RAW_DATA,
'AES-256-ECB'
);
}
/**
* Validate the request params.
*
* @param array $message
*
* @throws \ByteDance\Payment\Kernel\Exceptions\InvalidSignException
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
*/
protected function validate(array $message)
{
$sign = $message['msg_signature'];
unset($message['msg_signature']);
if (Support\generate_verify_sign($message, $this->app['config']->token) !== $sign) {
throw new InvalidSignException();
}
}
/**
* @param mixed $result
*/
protected function strict($result)
{
if (true !== $result && is_null($this->fail)) {
$this->fail(strval($result));
}
}
}

32
src/Payment/Notify/Paid.php

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Notify;
use Closure;
class Paid extends Handler
{
/**
* @param \Closure $closure
*
* @return \Symfony\Component\HttpFoundation\Response
*
*/
public function handle(Closure $closure)
{
$this->strict(
\call_user_func($closure, $this->getMessage(), [$this, 'fail'])
);
return $this->toResponse();
}
}

46
src/Payment/Notify/Refunded.php

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Notify;
use Closure;
class Refunded extends Handler
{
protected $check = false;
/**
* @param \Closure $closure
*
* @return \Symfony\Component\HttpFoundation\Response
*
*/
public function handle(Closure $closure)
{
$this->strict(
\call_user_func($closure, $this->getMessage(), $this->reqInfo(), [$this, 'fail'])
);
return $this->toResponse();
}
/**
* Decrypt the `req_info` from request message.
*
* @return array
*
* @throws \HttpBase\Exceptions\Exception
*/
public function reqInfo()
{
}
}

49
src/Payment/Order/Client.php

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Order;
use ByteDance\Payment\Kernel\BaseClient;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
* 下单
* @param $params
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function unify($params)
{
return $this->request('ecpay/v1/create_order', $params);
}
/**
* 订单查询
* @param $outOrderNo
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function query($outOrderNo)
{
$params['out_order_no'] = $outOrderNo;
return $this->request('ecpay/v1/query_order', $params);
}
}

31
src/Payment/Order/ServiceProvider.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Order;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['order']) && $app['order'] = function ($app) {
return new Client($app);
};
}
}

56
src/Payment/Refund/Client.php

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Refund;
use ByteDance\Payment\Kernel\BaseClient;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
* @param string $number
* @param string $refundNumber
* @param int $refundFee
* @param array $optional
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function byOutTradeNumber(string $number, string $refundNumber, int $refundFee, array $optional = [])
{
return $this->refund($refundNumber, $refundFee, array_merge($optional, ['out_order_no' => $number]));
}
/**
* @param string $refundNumber
* @param int $refundFee
* @param array $optional
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function refund(string $refundNumber, int $refundFee, $optional = [])
{
$params = array_merge([
'out_refund_no' => $refundNumber,
'refund_amount' => $refundFee
], $optional);
return $this->request('ecpay/v1/create_refund', $params);
}
}

31
src/Payment/Refund/ServiceProvider.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Refund;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['refund']) && $app['refund'] = function ($app) {
return new Client($app);
};
}
}

59
src/Payment/Settle/Client.php

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
<?php
/*
* This file is part of the OtkurBiz/ByteDance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Settle;
use ByteDance\Payment\Kernel\BaseClient;
/**
* Class Client.
*
* @author
*/
class Client extends BaseClient
{
/**
* 请求分账
* @param string $settleNumber
* @param string $orderNumber
* @param string $desc
* @param array $optional
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function settle(string $settleNumber, string $orderNumber, string $desc = '分账', array $optional = [])
{
$params = array_merge([
'out_settle_no' => $settleNumber,
'out_order_no' => $orderNumber,
'settle_desc' => $desc
], $optional);
return $this->request('ecpay/v1/settle', $params);
}
/**
* 请求分账
* @param string $settleNumber
* @return array|\ByteDance\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
* @throws \ByteDance\Kernel\Exceptions\InvalidArgumentException
* @throws \ByteDance\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function query(string $settleNumber, array $optional = [])
{
$params = array_merge([
'out_settle_no' => $settleNumber
], $optional);
return $this->request('ecpay/v1/settle', $params);
}
}

31
src/Payment/Settle/ServiceProvider.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the otkurbiz/bytedance.
*
* (c)
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace ByteDance\Payment\Settle;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['settle']) && $app['settle'] = function ($app) {
return new Client($app);
};
}
}

29
src/config/bytedance.php

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
<?php
/*
* This file is part of the overtrue/laravel-wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
return [
/*
* 默认配置,将会合并到各模块中
*/
'defaults' => [
/*
* 指定 API 调用返回结果的类型:array(default)/collection/object/raw/自定义类名
*/
'response_type' => 'array'
],
'mini_program' => [
'default' => [
'app_id' => '',
'app_secret' => '',
],
],
];
Loading…
Cancel
Save