diff --git a/src/Kernel/Client.php b/src/Kernel/Client.php index 9a194712a..e9336c468 100644 --- a/src/Kernel/Client.php +++ b/src/Kernel/Client.php @@ -6,34 +6,22 @@ use EasyWeChat\Kernel\Contracts\AccessToken as AccessTokenInterface; use EasyWeChat\Kernel\Contracts\AccessTokenAwareHttpClient as AccessTokenAwareHttpClientInterface; -use EasyWeChat\Kernel\Contracts\ChainableHttpClient as ChainableHttpClientInterface; -use EasyWeChat\Kernel\Traits\ChainableHttpClient; +use JetBrains\PhpStorm\Pure; use Symfony\Component\HttpClient\DecoratorTrait; use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; -/** - * @method \Symfony\Contracts\HttpClient\ResponseInterface get(string|array $uri = [], array $options = []) - * @method \Symfony\Contracts\HttpClient\ResponseInterface post(string|array $uri = [], array $options = []) - * @method \Symfony\Contracts\HttpClient\ResponseInterface patch(string|array $uri = [], array $options = []) - * @method \Symfony\Contracts\HttpClient\ResponseInterface put(string|array $uri = [], array $options = []) - * @method \Symfony\Contracts\HttpClient\ResponseInterface delete(string|array $uri = [], array $options = []) - */ -class Client implements AccessTokenAwareHttpClientInterface, ChainableHttpClientInterface +class Client implements AccessTokenAwareHttpClientInterface { use DecoratorTrait; - use ChainableHttpClient; - - protected ?AccessTokenInterface $accessToken; public function __construct( ?HttpClientInterface $client = null, - string $uri = '/', - ?AccessTokenInterface $accessToken = null, + protected ?AccessTokenInterface $accessToken = null, ) { - $this->uri = $uri; $this->client = $client ?? HttpClient::create(); - $this->accessToken = $accessToken; } public function withAccessToken(AccessTokenInterface $accessToken): static @@ -56,4 +44,104 @@ public function request(string $method, string $url, array $options = []): \Symf return $this->client->request($method, ltrim($url, '/'), $options); } + + /** + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + */ + public function get(string $url, array $options = []): \Symfony\Contracts\HttpClient\ResponseInterface + { + return $this->request('GET', $url, $options); + } + + /** + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + */ + public function post(string $url, array $options = []): \Symfony\Contracts\HttpClient\ResponseInterface + { + if (!\array_key_exists('body', $options) && !\array_key_exists('json', $options)) { + $options['body'] = $options; + } + + return $this->request('POST', $url, $options); + } + + /** + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + */ + public function patch(string $url, array $options = []): \Symfony\Contracts\HttpClient\ResponseInterface + { + if (!\array_key_exists('body', $options) && !\array_key_exists('json', $options)) { + $options['body'] = $options; + } + + return $this->request('PATCH', $url, $options); + } + + /** + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + */ + public function put(string $url, array $options = []): \Symfony\Contracts\HttpClient\ResponseInterface + { + if (!\array_key_exists('body', $options) && !\array_key_exists('json', $options)) { + $options['body'] = $options; + } + + return $this->request('PUT', $url, $options); + } + + /** + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + */ + public function delete(string $url, array $options = []): \Symfony\Contracts\HttpClient\ResponseInterface + { + return $this->request('DELETE', $url, $options); + } + + public function __call(string $name, array $arguments) + { + return \call_user_func_array([$this->client, $name], $arguments); + } + + public static function mock(string $response = '', ?int $status = 200, ?string $contentType = 'application/json', array $headers = [], string $baseUri = 'https://example.com'): object + { + $mockResponse = new MockResponse( + $response, + array_merge([ + 'http_code' => $status, + 'content_type' => $contentType, + ], $headers) + ); + + return new class ($mockResponse, $baseUri) { + use DecoratorTrait; + + public function __construct(public MockResponse $mockResponse, $baseUri) + { + $this->client = new Client(new MockHttpClient($this->mockResponse, $baseUri)); + } + + public function __call(string $name, array $arguments) + { + return \call_user_func_array([$this->client, $name], $arguments); + } + + #[Pure] + public function getRequestMethod() + { + return $this->mockResponse->getRequestMethod(); + } + + #[Pure] + public function getRequestUrl() + { + return $this->mockResponse->getRequestUrl(); + } + + #[Pure] + public function getRequestOptions() + { + return $this->mockResponse->getRequestOptions(); + } + }; + } } diff --git a/src/Kernel/Contracts/ChainableHttpClient.php b/src/Kernel/Contracts/ChainableHttpClient.php deleted file mode 100644 index 1f74654e0..000000000 --- a/src/Kernel/Contracts/ChainableHttpClient.php +++ /dev/null @@ -1,13 +0,0 @@ -uri = $uri; - } else { - $uri = \preg_match('~\$[a-z0-9_]+~i', $uri) ? $uri : Str::kebab($uri); - $clone->uri = \trim(\sprintf('/%s/%s', \trim($this->uri, '/'), \trim($uri, '/')), '/'); - } - - return $clone; - } - - public function getUri(): string - { - return $this->uri; - } - - public function __get(string | int $name): static - { - return $this->withUri(\strval($name)); - } - - /** - * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException - * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface - */ - public function __call(string $name, array $arguments) - { - if (\in_array(\strtoupper($name), ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])) { - return $this->callWithShortcuts(\strtoupper($name), ...$arguments); - } - - return \call_user_func_array([$this->client, $name], $arguments); - } - - /** - * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException - * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface - */ - protected function callWithShortcuts( - string $method, - string | array $uri = [], - array $options = [] - ): \Symfony\Contracts\HttpClient\ResponseInterface { - if (\is_string($uri)) { - $uri = $this->withUri($uri)->getUri(); - } else { - $options = $uri; - $uri = $this->getUri(); - } - - [$uri, $options] = $this->replaceUriVariables($uri, $options); - - return $this->request(\strtoupper($method), $uri, $options); - } - - /** - * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException - */ - public function replaceUriVariables(string $uri, array $options): array - { - return [ - \preg_replace_callback( - pattern : '~\$(?[a-z0-9_]+)~i', - callback: function (array $matches) use (&$options): string { - if (empty($options[$matches['name']])) { - throw new InvalidArgumentException(\sprintf('Missing url variables "%s".', $matches['name'])); - } - - $value = $options[$matches['name']]; - - unset($options[$matches['name']]); - - return $value; - }, - subject : $uri - ), - $options, - ]; - } -} diff --git a/src/OfficialAccount/Application.php b/src/OfficialAccount/Application.php index e6dc4a1fb..c2ffa9de9 100644 --- a/src/OfficialAccount/Application.php +++ b/src/OfficialAccount/Application.php @@ -186,7 +186,7 @@ public function getUtils(): Utils public function createClient(): Client { - return new Client($this->getHttpClient(), '', $this->getAccessToken()); + return new Client($this->getHttpClient(), $this->getAccessToken()); } protected function getHttpClientDefaultOptions(): array diff --git a/src/OpenPlatform/Application.php b/src/OpenPlatform/Application.php index 049a57001..7488e63bc 100644 --- a/src/OpenPlatform/Application.php +++ b/src/OpenPlatform/Application.php @@ -268,7 +268,7 @@ protected function createAuthorizerOAuthFactory(string $authorizerAppId, Officia public function createClient(): Client { - return new Client($this->getHttpClient(), '', $this->getComponentAccessToken()); + return new Client($this->getHttpClient(), $this->getComponentAccessToken()); } protected function getHttpClientDefaultOptions(): array diff --git a/src/Pay/Application.php b/src/Pay/Application.php index f4047abe1..f5837a890 100644 --- a/src/Pay/Application.php +++ b/src/Pay/Application.php @@ -18,8 +18,7 @@ class Application implements \EasyWeChat\Pay\Contracts\Application use InteractWithServerRequest; protected ?ServerInterface $server = null; - protected ?HttpClientInterface $v2Client = null; - protected ?HttpClientInterface $v3Client = null; + protected ?HttpClientInterface $client = null; protected ?Merchant $merchant = null; public function getMerchant(): Merchant @@ -37,29 +36,6 @@ public function getMerchant(): Merchant return $this->merchant; } - public function decorateMerchantAwareHttpClient(HttpClientInterface $httpClient): MerchantAwareHttpClient - { - return new MerchantAwareHttpClient($this->getMerchant(), $httpClient, $this->config->get('http', [])); - } - - public function getClient(): HttpClientInterface - { - if (!$this->v3Client) { - $this->v3Client = $this->decorateMerchantAwareHttpClient($this->getHttpClient())->withUri('v3'); - } - - return $this->v3Client; - } - - public function getV2Client(): HttpClientInterface - { - if (!$this->v2Client) { - $this->v2Client = $this->decorateMerchantAwareHttpClient($this->getHttpClient()); - } - - return $this->v2Client; - } - public function getUtils(): Utils { return new Utils($this->getMerchant()); @@ -100,4 +76,14 @@ public function getConfig(): ConfigInterface { return $this->config; } + + public function getClient(): HttpClientInterface + { + return $this->client ?? $this->client = $this->decorateMerchantAwareHttpClient($this->getHttpClient()); + } + + protected function decorateMerchantAwareHttpClient(HttpClientInterface $httpClient): MerchantAwareHttpClient + { + return new MerchantAwareHttpClient($this->getMerchant(), $httpClient, $this->config->get('http', [])); + } } diff --git a/src/Pay/Contracts/Application.php b/src/Pay/Contracts/Application.php index d141e54ce..7b1a033ee 100644 --- a/src/Pay/Contracts/Application.php +++ b/src/Pay/Contracts/Application.php @@ -13,5 +13,4 @@ public function getMerchant(): Merchant; public function getConfig(): Config; public function getHttpClient(): HttpClientInterface; public function getClient(): HttpClientInterface; - public function getV2Client(): HttpClientInterface; } diff --git a/src/Pay/MerchantAwareHttpClient.php b/src/Pay/MerchantAwareHttpClient.php index aed05287e..48de655d6 100644 --- a/src/Pay/MerchantAwareHttpClient.php +++ b/src/Pay/MerchantAwareHttpClient.php @@ -4,9 +4,7 @@ namespace EasyWeChat\Pay; -use EasyWeChat\Kernel\Contracts\ChainableHttpClient as ChainableHttpClientInterface; use EasyWeChat\Kernel\Support\UserAgent; -use EasyWeChat\Kernel\Traits\ChainableHttpClient; use Nyholm\Psr7\Stream; use Nyholm\Psr7\Uri; use Symfony\Component\HttpClient\HttpClient as SymfonyHttpClient; @@ -16,10 +14,9 @@ use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface; -class MerchantAwareHttpClient implements HttpClientInterface, ChainableHttpClientInterface +class MerchantAwareHttpClient implements HttpClientInterface { use HttpClientTrait; - use ChainableHttpClient; protected HttpClientInterface $client; diff --git a/src/Work/Application.php b/src/Work/Application.php index 61900fbba..b06f4acb4 100644 --- a/src/Work/Application.php +++ b/src/Work/Application.php @@ -119,7 +119,7 @@ public function setAccessToken(AccessTokenInterface $accessToken): static public function createClient(): Client { - return new Client($this->getHttpClient(), '', $this->getAccessToken()); + return new Client($this->getHttpClient(), $this->getAccessToken()); } public function getOAuth(): WeWork diff --git a/tests/Kernel/ClientTest.php b/tests/Kernel/ClientTest.php index 0b52a5ca1..499ba54f3 100644 --- a/tests/Kernel/ClientTest.php +++ b/tests/Kernel/ClientTest.php @@ -6,69 +6,83 @@ use EasyWeChat\Kernel\Client; use EasyWeChat\Tests\TestCase; -use Symfony\Contracts\HttpClient\HttpClientInterface; class ClientTest extends TestCase { - public function test_uri_appends() - { - // without basic uri - $client = new Client(); - - // basic - $this->assertSame('v3/pay/transactions/native', actual: $client->v3->pay->transactions->native->getUri()); - - // camel-case - $this->assertSame('v3/merchant-service', $client->v3->merchantService->getUri()); - - // variable - $merchantId = 11000000; - $this->assertSame( - "v3/combine-transactions/out-trade-no/{$merchantId}/close", - $client->v3->combineTransactions->outTradeNo->$merchantId->close->getUri() - ); - - // with basic uri - $client = new Client(uri: 'v3/pay/'); - - $this->assertSame('v3/pay/transactions/native', actual: $client->transactions->native->getUri()); - } - public function test_full_uri_call() { - $client = \Mockery::mock(HttpClientInterface::class); - $client = new Client(client: $client, uri: 'v3'); - - $client->expects()->request('GET', 'https://api2.mch.weixin.qq.com/v3/certificates', [])->once(); - $client->get('https://api2.mch.weixin.qq.com/v3/certificates'); - + $client = Client::mock(); $options = [ 'headers' => [ 'accept' => 'application/json', ], ]; - $client->expects()->request('GET', 'https://api2.mch.weixin.qq.com/v3/certificates', $options)->once(); - $client->get('https://api2.mch.weixin.qq.com/v3/certificates', $options); + $client->request('GET', 'https://api2.mch.weixin.qq.com/v3/certificates', $options); + + $this->assertSame('GET', $client->getRequestMethod()); + $this->assertSame('https://api2.mch.weixin.qq.com/v3/certificates', $client->getRequestUrl()); + $this->assertSame(['accept: application/json'], $client->getRequestOptions()['headers']); } public function test_shortcuts_call() { - $client = \Mockery::mock(HttpClientInterface::class); - $client = new Client(client: $client, uri: 'v3'); - - $client->expects()->request('GET', 'v3/certificates', [])->once(); - $client->get('certificates'); - + $client = Client::mock(); - $options = [ + $client->get('v3/certificates', [ 'headers' => [ 'accept' => 'application/json', ], - ]; - $client->expects()->request('GET', 'v3/certificates', $options)->once(); + ]); - $client->get('certificates', $options); + $this->assertSame('GET', $client->getRequestMethod()); + $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl()); + $this->assertSame(['accept: application/json'], $client->getRequestOptions()['headers']); + } + + public function test_it_will_auto_wrap_body() + { + $client = Client::mock(); + + $client->post('v3/certificates', [ + 'body' => [ + 'foo' => 'bar', + ], + ]); + + $this->assertSame('POST', $client->getRequestMethod()); + $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl()); + $this->assertSame('foo=bar', $client->getRequestOptions()['body']); + + // post without body key + $client = Client::mock(); + $client->post('v3/certificates', [ + 'foo' => 'bar', + ]); + + $this->assertSame('POST', $client->getRequestMethod()); + $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl()); + $this->assertSame('foo=bar', $client->getRequestOptions()['body']); + + // patch without body key + $client = Client::mock(); + $client->patch('v3/certificates', [ + 'foo' => 'bar', + ]); + + $this->assertSame('PATCH', $client->getRequestMethod()); + $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl()); + $this->assertSame('foo=bar', $client->getRequestOptions()['body']); + + // put without body key + $client = Client::mock(); + $client->put('v3/certificates', [ + 'foo' => 'bar', + ]); + + $this->assertSame('PUT', $client->getRequestMethod()); + $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl()); + $this->assertSame('foo=bar', $client->getRequestOptions()['body']); } } diff --git a/tests/Pay/ApplicationTest.php b/tests/Pay/ApplicationTest.php index fcdd44989..493ddfb90 100644 --- a/tests/Pay/ApplicationTest.php +++ b/tests/Pay/ApplicationTest.php @@ -43,40 +43,4 @@ public function test_get_client() $this->assertInstanceOf(CurlHttpClient::class, $app->getHttpClient()); $this->assertSame($app->getHttpClient(), $app->getHttpClient()); } - - public function test_get_v3_client() - { - $app = new Application( - [ - 'mch_id' => 101111111, - 'secret_key' => 'mock-secret-key', - 'private_key' => 'mock-private-key', - 'certificate' => '/path/to/certificate.cert', - 'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO', - ] - ); - - $this->assertInstanceOf(MerchantAwareHttpClient::class, $app->getClient()); - $this->assertSame($app->getClient(), $app->getClient()); - - $this->assertSame('v3', $app->getClient()->getUri()); - } - - public function test_get_v2_client() - { - $app = new Application( - [ - 'mch_id' => 101111111, - 'secret_key' => 'mock-secret-key', - 'private_key' => 'mock-private-key', - 'certificate' => '/path/to/certificate.cert', - 'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO', - ] - ); - - $this->assertInstanceOf(MerchantAwareHttpClient::class, $app->getV2Client()); - $this->assertSame($app->getV2Client(), $app->getV2Client()); - - $this->assertSame('/', $app->getV2Client()->getUri()); - } }